Webhooks und Serverless Funktionen für Kampagnen, die auf deinen Shop reagieren

Kurz gesagt

Niveau: Fortgeschritten

Ein Webhook ist der Shop, der dir sofort sagt, dass sich etwas geändert hat. Eine Serverless Funktion ist ein kleines Stück Code, das darauf reagiert.

  • Prüf die Signatur jedes Webhooks am rohen Body, bevor du ihm glaubst.
  • Antworte in Sekunden und leg die eigentliche Arbeit in eine Warteschlange, denn Shopify gibt dir fünf Sekunden.
  • Webhooks kommen doppelt, zu spät und in falscher Reihenfolge, also mach jede Aktion wiederholbar und ignorier veraltete Änderungen.
  • Wiederhole mit wachsenden Wartezeiten, schick dauerhafte Fehler in eine Dead Letter Queue und gleich jede Nacht ab, was gefehlt hat.
  • Prüf vor jeder Erinnerungsmail die Erlaubnis nach dem UWG und lass die erwartete Marge entscheiden, nicht eine feste Schwelle, ob es Rabatt gibt.

Anzeigen, die weiterlaufen, nachdem der Bestseller ausverkauft ist, und Rabattbanner, die die Aktion überleben, sind ein Problem des Stundenplans. In diesem Artikel zeige ich, wie Webhooks und Serverless Funktionen einen erfundenen Kerzenshop in Hamburg sofort reagieren lassen, wenn sich etwas ändert, und wie du das so baust, dass Duplikate, verspätete Nachrichten und verpasste Ereignisse ihn nie das Falsche tun lassen.

Wenn sich im Shop etwas ändert, sollte das Marketing es erfahren

Das meiste Marketing läuft nach Stundenplan. Jemand prüft am Montag den Bestand, pausiert die Anzeige für die Kerze, die am Samstag ausverkauft war, und merkt am Mittwoch, dass die Rabattaktion schon gestern Abend zu Ende ging. Dazwischen versprechen die Anzeigen Dinge, die der Shop nicht mehr liefern kann.

Besser ist es, wenn der Shop dem Marketing selbst Bescheid gibt, sobald sich etwas ändert, über einen Webhook, und wenn ein kleines Stück Code bereitsteht, das darauf reagiert, meistens eine Serverless Funktion. In diesem Artikel verbinde ich beides so, dass jedes Mal das Richtige passiert, auch wenn die Nachricht doppelt kommt, zu spät oder gar nicht.

Dabei begleitet mich ein erfundenes Unternehmen. Elbkerzen ist ein Onlineshop für Kerzen in Hamburg. Jonas führt ihn auf Shopify, unter elbkerzen.example. Der Bestseller ist die Kerze Winterzauber für 24 €, und sie hat in Google Ads eine eigene Anzeigengruppe. Niemand davon ist echt, und jede Zahl unten ist ausgedacht, damit ich jeden Schritt zeigen kann.

Ein Webhook ist der Shop, der dich anruft, statt dass du ihn anrufst

Du kannst ein anderes System immer wieder fragen: "Gibt es etwas Neues?" Das nennt man Polling. Oder das System meldet sich, sobald etwas passiert. Das ist ein Webhook: Der Shop schickt eine HTTP Anfrage (eine kleine Nachricht über das Web, so wie ein Browser eine Seite lädt) an eine Adresse, die du ihm gegeben hast, und die Einzelheiten stehen im Body.

Shopify nennt seine Ereignisse Topics. Jede Zustellung trägt Header, die sagen, worum es geht: das Topic, etwa inventory_levels/update, den Shop, von dem sie kommt, eine Zustellungs ID, den Zeitpunkt, an dem sie ausgelöst wurde, und eine Signatur.1 Der Body ist JSON. Bei einer Bestandsänderung ist er kurz: welcher Lagerartikel, an welchem Standort, wie viele verfügbar sind und wann das aktualisiert wurde.1

Polling hat seinen ehrlichen Platz. Wenn du alle II Minuten nachsiehst und eine Änderung jederzeit passieren kann, fällt sie im Schnitt genau zwischen zwei Prüfungen.

dˉ=I2\bar{d} = \frac{I}{2}

Dabei ist dˉ\bar{d} die durchschnittliche Verzögerung, bis du eine Änderung bemerkst, in Minuten, und II der Abstand zwischen zwei Abfragen, ebenfalls in Minuten. Es ist der Mittelwert einer Verzögerung, die mit gleicher Wahrscheinlichkeit irgendwo zwischen 00 und II liegt, von Grund auf erklärt in Grundlagen: der Mittelwert.

Rechenbeispiel: was der ausverkaufte Winterzauber kostet

Die Anzeigengruppe für Winterzauber bekommt 40 Klicks am Tag zu 0,60 € pro Klick. An einem Samstag um 14 Uhr geht die letzte Kerze weg. Jonas sieht sich seine Anzeigen am Montag um 9 Uhr an, 43 Stunden später. Die Klicks, die er in der Zwischenzeit kauft, landen alle auf einer ausverkauften Seite:

4024×43×0,60=43\frac{40}{24} \times 43 \times 0{,}60 = 43

Dabei ist 4024\frac{40}{24} die Zahl der Klicks pro Stunde, 4343 die Zahl der Stunden, bis Jonas nachsieht, und 0,600{,}60 der Preis pro Klick in Euro. Das sind 43 € für Klicks, aus denen keine Bestellung werden konnte.

Jetzt ein Skript, das den Bestand alle 15 Minuten abfragt:

dˉ=152=7,5\bar{d} = \frac{15}{2} = 7{,}5

Dabei ist dˉ\bar{d} eine durchschnittliche Blindzeit von 7,5 Minuten. In 7,5 Minuten kauft die Anzeigengruppe 7,5×401.440=0,217{,}5 \times \frac{40}{1.440} = 0{,}21 Klicks, etwa 12 Cent. Ein Webhook drückt das auf wenige Sekunden und spart Jonas noch einmal 12 Cent.

Ausgaben für Klicks auf ein ausverkauftes Produkt, je nachdem, wie der Bestand geprüft wirdWer am Montagmorgen von Hand nachsieht, zahlt 43 € für Klicks auf eine ausverkaufte Seite, eine Abfrage alle 15 Minuten kostet etwa 12 Cent, und ein Webhook kostet fast nichts.Elbkerzen, ausverkaufter Winterzauber, Ausgaben bis die Anzeigengruppe pausiert ist0,00 €12,50 €25,00 €37,50 €50,00 €43,00 €Jonas sieht amMontag nach0,12 €Skript fragtalle 15 Minutenab0,01 €WebhookVerlorene Ausgaben (€)

Jeder Balken ist eine Art, den Ausverkauf zu bemerken. Die senkrechte Achse zeigt das Geld, das für Klicks ausgegeben wurde, zwischen dem Verkauf der letzten Kerze und dem Pausieren der Anzeigengruppe. Die Erkenntnis: Fast die ganze Ersparnis kommt daher, dass die Prüfung überhaupt automatisch läuft, nicht vom Webhook. Für ein einzelnes Produkt reicht eine Abfrage alle 15 Minuten. Webhooks lohnen sich, wenn du Tausende Dinge beobachtest (2.000 Produkte alle 15 Minuten abzufragen sind 192.000 Anfragen am Tag) oder innerhalb einer Minute reagieren musst, etwa auf einen Checkout, den jemand gerade verlassen hat.

Serverless heißt, jemand anderes hält den Server am Laufen, mit Grenzen

Ein Webhook braucht eine Adresse, die immer zuhört. Eine Serverless Funktion liefert sie ohne eigenen Server: Du lädst eine Funktion hoch, der Anbieter führt sie aus, sobald eine Anfrage eintrifft, und du zahlst pro Ausführung. Die zwei, die ich am häufigsten nutze, sind AWS Lambda und Cloudflare Workers.

Bei den Grenzen wird die Werbung der Anbieter leise. Eine Lambda Funktion läuft höchstens 15 Minuten, mit 128 MB bis 10.240 MB Arbeitsspeicher, und eine synchrone Anfrage oder Antwort darf höchstens 6 MB groß sein. Ein Konto startet standardmäßig mit 1.000 gleichzeitigen Ausführungen, und AWS weist darauf hin, dass neue Konten niedriger anfangen und mit der Nutzung angehoben werden.2 Ein Cloudflare Worker bekommt im kostenlosen Tarif 10 ms CPU Zeit pro Anfrage, im bezahlten Tarif standardmäßig 30 Sekunden, bis zu fünf Minuten, jeweils mit 128 MB Arbeitsspeicher. Der kostenlose Tarif endet bei 100.000 Anfragen am Tag.3 "Skaliert sofort", wie die alte Fassung dieses Artikels versprach, stimmt also bis zu einem Kontingent, und das solltest du vor dem Weihnachtsgeschäft wissen, nicht mittendrin.

Rechenbeispiel: die Monatsrechnung von Elbkerzen

Elbkerzen bekommt etwa 30.000 Webhooks im Monat, weil jede Bestellung den Bestand ändert. Jeder löst zwei Funktionen aus: einen Empfänger mit 128 MB für 0,1 Sekunden und einen Worker mit 256 MB für 0,5 Sekunden. Lambda rechnet die Rechenzeit in Gigabyte Sekunden ab, also Arbeitsspeicher in GB mal Laufzeit in Sekunden:

30.000×0,125×0,1+30.000×0,25×0,5=4.12530.000 \times 0{,}125 \times 0{,}1 + 30.000 \times 0{,}25 \times 0{,}5 = 4.125

Dabei ist 30.00030.000 die Zahl der Ausführungen pro Funktion, 0,1250{,}125 und 0,250{,}25 sind der Arbeitsspeicher in GB, 0,10{,}1 und 0,50{,}5 die Sekunden pro Ausführung. Das ergibt 4.125 GB Sekunden und 60.000 Anfragen im Monat. Das kostenlose Kontingent von Lambda deckt eine Million Anfragen und 400.000 GB Sekunden im Monat.4 Jonas' Funktionen brauchen also etwa 1 % der Rechenzeit und 6 % der Anfragen davon. Selbst ohne dieses Kontingent kämen beim Listenpreis von AWS, 0,20 US Dollar pro Million Anfragen und 0,0000166667 US Dollar pro GB Sekunde in der Region US East, etwa 8 US Cent zusammen.4 Die Warteschlange und die Datenbank, die ich weiter unten ergänze, werden getrennt abgerechnet, aber in dieser Größe ist die Rechnung nicht die Kosten. Die Kosten liegen darin, es ordentlich zu bauen.

Prüf, ob die Nachricht echt ist, bevor du ihr glaubst

Eine Webhook Adresse ist öffentlich. Jeder, der sie findet, kann eine Anfrage schicken, die aussieht wie Shopify mit der Meldung "Winterzauber ist ausverkauft". Die alte Fassung dieses Artikels hätte ihr geglaubt, weil ihr Code auf den Body reagierte, ohne irgendetwas zu prüfen.

Shopify signiert jede Zustellung. Es berechnet einen HMAC (Hash based Message Authentication Code: einen Fingerabdruck der Nachricht, erzeugt mit einem Geheimnis, das nur du und Shopify kennen) über den unveränderten Request Body, mit SHA256 und dem Client Secret deiner App, und schickt ihn Base64 kodiert im Header X-Shopify-Hmac-Sha256. Du berechnest auf deiner Seite denselben Fingerabdruck und vergleichst. Shopify betont, dass du den rohen Body nehmen musst, bevor irgendein Framework ihn verarbeitet, sonst passen die Fingerabdrücke nie zusammen.5

Stripe nimmt zusätzlich einen Zeitstempel dazu. Sein Header Stripe-Signature signiert den Zeitstempel, einen Punkt und den rohen Body gemeinsam, und die Bibliotheken von Stripe lehnen standardmäßig alles ab, was älter als fünf Minuten ist. Eine abgefangene Nachricht lässt sich so nächste Woche nicht noch einmal abspielen.6

Hier ist der Empfänger von Elbkerzen als Lambda Funktion hinter einer Funktions URL. Lambda übergibt den Body als Zeichenkette, Base64 kodiert, wenn der Inhalt binär ist. Die erste Aufgabe ist also, genau die ursprünglichen Bytes zurückzubekommen.7

import base64
import hashlib
import hmac
import os

import boto3

sqs = boto3.client("sqs")
QUEUE_URL = os.environ["QUEUE_URL"]
SECRET = os.environ["SHOPIFY_CLIENT_SECRET"].encode()


def lambda_handler(event, context):
    headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()}
    body = event.get("body") or ""
    raw = base64.b64decode(body) if event.get("isBase64Encoded") else body.encode("utf-8")

    expected = base64.b64encode(hmac.new(SECRET, raw, hashlib.sha256).digest()).decode()
    if not hmac.compare_digest(expected, headers.get("x-shopify-hmac-sha256", "")):
        return {"statusCode": 401, "body": "invalid signature"}

    sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=raw.decode("utf-8"),
        MessageAttributes={
            "topic": {"DataType": "String", "StringValue": headers["x-shopify-topic"]},
            "webhook_id": {"DataType": "String", "StringValue": headers["x-shopify-webhook-id"]},
        },
    )
    return {"statusCode": 200, "body": "ok"}

hmac.compare_digest vergleicht in konstanter Zeit, damit niemand die Signatur herausfinden kann, indem er misst, wie lange falsche Versuche dauern. Und die Funktion tut nichts weiter, als die Nachricht in eine Warteschlange zu legen, um die es im nächsten Abschnitt geht.

Rechenbeispiel: eine geänderte Ziffer

Damit es lesbar bleibt, nehme ich ein ausgedachtes Geheimnis, demo_secret, und einen gekürzten Body: {"inventory_item_id":808950810,"available":0}. Der HMAC SHA256 dieses Bodys mit diesem Schlüssel, Base64 kodiert, ist WsbEAO23WBTv/jRCvwSm+JKbk0mqDEnVoHet6eHzjW8=. Genau das würde Shopify in den Header schreiben.

Jetzt schickt jemand dieselbe Anfrage mit "available":9, in der Hoffnung, eine Anzeigengruppe wieder einzuschalten. Der Fingerabdruck dieses Bodys ist 8VaGxE/VorQ4ejSaiw7t2BCxAFr1PMnPTAxhDkce51w=, keinerlei Ähnlichkeit mit dem ersten. Der Vergleich schlägt fehl, der Empfänger antwortet mit 401, nichts passiert. Ohne das Geheimnis kann niemand den passenden Fingerabdruck für einen selbst gewählten Body erzeugen.

Auf Cloudflare sieht der Empfänger in JavaScript fast genauso aus. Workers unterstützen HMAC über die Web Crypto API, und Cloudflare Queues nimmt die Nachricht entgegen.89

export default {
  async fetch(request, env) {
    if (request.method !== "POST") return new Response("method not allowed", { status: 405 });
    const raw = await request.arrayBuffer();

    let signature;
    try {
      const header = request.headers.get("X-Shopify-Hmac-Sha256") || "";
      signature = Uint8Array.from(atob(header), (c) => c.charCodeAt(0));
    } catch {
      return new Response("invalid signature", { status: 401 });
    }

    const key = await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(env.SHOPIFY_CLIENT_SECRET),
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["verify"],
    );
    if (!(await crypto.subtle.verify("HMAC", key, signature, raw))) {
      return new Response("invalid signature", { status: 401 });
    }

    await env.SHOP_EVENTS.send({
      topic: request.headers.get("X-Shopify-Topic"),
      webhookId: request.headers.get("X-Shopify-Webhook-Id"),
      body: new TextDecoder().decode(raw),
    });
    return new Response("ok");
  },
};

Ich würde die ganze Kette auf einer Plattform halten, statt sie wie die alte Fassung auf zwei zu verteilen: Zwei Clouds bedeuten zwei Stellen, an denen du Logs durchsuchen musst. Ab hier bleibe ich bei AWS.

Antworte in Sekunden, arbeite danach

Shopify wartet fünf Sekunden auf deine Antwort. Kommt in dieser Zeit keine Erfolgsmeldung, gilt die Zustellung als fehlgeschlagen. Shopify wiederholt fehlgeschlagene Zustellungen bis zu achtmal innerhalb von vier Stunden, und wenn die Fehler anhalten, löscht es das Abonnement ganz.10 Stripe rät, eine Erfolgsmeldung zurückzugeben, bevor irgendeine aufwendige Logik läuft, Ereignisse aus einer Warteschlange zu verarbeiten, und wiederholt im Livemodus bis zu drei Tage lang.6

Der Empfänger prüft also nur die Signatur, schreibt die Nachricht in eine Warteschlange und antwortet. Eine zweite Funktion, der Worker, holt die Nachrichten aus der Warteschlange und erledigt die langsame Arbeit in seinem eigenen Tempo. Auf AWS ist diese Warteschlange SQS (Simple Queue Service), die Lambda direkt auslesen kann.

Von einem Shopify Ereignis zur pausierten AnzeigengruppeShopify schickt einen Webhook an eine Empfängerfunktion, die die Signatur prüft und ihn entweder ablehnt oder in eine Warteschlange legt und innerhalb von Sekunden antwortet. Eine Workerfunktion holt ihn aus der Warteschlange, überspringt ihn, wenn er älter ist als die letzte bekannte Änderung, handelt sonst und schickt Nachrichten, die immer wieder scheitern, in eine Dead Letter Queue.Bestand ändert sich in Shopify, Shopifyschickt einen WebhookEmpfängerfunktion prüft die HMACSignaturStimmt die Signatur?neinMit 401 antworten und nichts tunjaNachricht in die Warteschlange legenund innerhalb von Sekunden mit 200antwortenWorkerfunktion holt die Nachricht ausder WarteschlangeIst sie neuer als die letztebekannte Änderung für diesenArtikel?neinÜberspringen, eine spätere Änderung hatschon gewonnenjaAnzeigengruppe pausieren odereinschalten, bei Fehlern mit wachsenderWartezeit wiederholenNach fünf gescheiterten Versuchenlandet die Nachricht in der Dead LetterQueue

Lies es von oben nach unten. Jedes Kästchen ist ein Schritt, jede Raute eine Frage. Die Erkenntnis ist die Teilung: Oberhalb der Warteschlange geht es schnell und einfach zu, unterhalb langsam und sorgfältig, und Shopify wartet nur auf die schnelle Hälfte.

Rechenbeispiel: ein langsamer Montag bei Google

An einem Montagmorgen braucht die Google Ads API acht Sekunden für eine Antwort. Hätte der Empfänger sie direkt aufgerufen, hätte Shopify nach fünf Sekunden aufgegeben und die Zustellung erneut geschickt, während der erste Versuch die Anzeigengruppe noch pausierte. Mit der Warteschlange dazwischen hat der Empfänger nach einer Zehntelsekunde geantwortet, und der Worker hat sich seine acht Sekunden genommen, ohne dass jemand warten musste.

Jede Nachricht kann doppelt kommen, zu spät oder in falscher Reihenfolge

Die alte Fassung dieses Artikels hat diesen Teil komplett ausgelassen, und genau er entscheidet, ob man dem Ganzen trauen kann.

Webhooks werden mindestens einmal zugestellt, nicht genau einmal. Shopify schreibt, dass dieselbe Zustellung mehr als einmal ankommen kann, und rät, Webhooks idempotent zu verarbeiten oder Wiederholungen anhand des Headers X-Shopify-Webhook-Id zu überspringen.11 Stripe sagt dasselbe, und dazu, dass Ereignisse nicht in der Reihenfolge ankommen, in der sie passiert sind.6 Auch Shopify garantiert keine Reihenfolge, verweist auf den Zeitstempel X-Shopify-Triggered-At und empfiehlt Abgleichsjobs, die ab und zu den tatsächlichen Stand abrufen, statt sich nur auf Webhooks zu verlassen.12 Die Warteschlange in der Mitte liefert ebenfalls mindestens einmal: SQS kann dir in seltenen Fällen dieselbe Nachricht zweimal geben.13

Der Worker muss also mit drei Dingen fertig werden: einem Duplikat, einer veralteten Nachricht, die eine frische überholt, und einer Nachricht, die nie kommt.

Eine Aktion ist idempotent, wenn sie zweimal ausgeführt dasselbe Ergebnis hinterlässt wie einmal. "Setz diese Anzeigengruppe auf pausiert" ist idempotent: Eine pausierte Anzeigengruppe zu pausieren ändert nichts. "Schick diesem Kunden eine Erinnerungsmail" ist es nicht: Zweimal heißt zwei Mails. Die erste Art braucht keine Deduplizierung. Die zweite braucht sie.

Die Reihenfolge ist ein eigenes Problem. Kommt "available: 0" nach "available: 12" an, pausiert ein Worker, der einfach der letzten empfangenen Nachricht folgt, die Anzeigengruppe für ein Produkt, das auf Lager ist. Der Schutz dagegen: Merk dir pro Artikel den neuesten Zeitstempel, den du gesehen hast, und ignorier alles, was älter ist. DynamoDB, die Key Value Datenbank von AWS, kann diese Prüfung und das Schreiben in einem Schritt erledigen, mit einem Bedingungsausdruck, der den Schreibvorgang scheitern lässt, wenn die Bedingung nicht erfüllt ist.14

import json
import os
from datetime import datetime

import boto3
from botocore.exceptions import ClientError
from google.ads.googleads.client import GoogleAdsClient
from google.api_core import protobuf_helpers

dynamodb = boto3.client("dynamodb")
STATE_TABLE = os.environ["STATE_TABLE"]
CUSTOMER_ID = os.environ["GOOGLE_ADS_CUSTOMER_ID"]
WATCHED = json.loads(os.environ["WATCHED_ITEMS"])  # {"808950810": {"ad_group_id": "...", "pause_at": 0}}
ads = GoogleAdsClient.load_from_env()


def is_newest(item_id, updated_epoch):
    try:
        dynamodb.update_item(
            TableName=STATE_TABLE,
            Key={"item_id": {"S": item_id}},
            UpdateExpression="SET last_seen = :t",
            ConditionExpression="attribute_not_exists(last_seen) OR last_seen <= :t",
            ExpressionAttributeValues={":t": {"N": str(updated_epoch)}},
        )
        return True
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            return False
        raise


def set_ad_group_paused(ad_group_id, paused):
    service = ads.get_service("AdGroupService")
    operation = ads.get_type("AdGroupOperation")
    ad_group = operation.update
    ad_group.resource_name = service.ad_group_path(CUSTOMER_ID, ad_group_id)
    status = ads.enums.AdGroupStatusEnum
    ad_group.status = status.PAUSED if paused else status.ENABLED
    ads.copy_from(operation.update_mask, protobuf_helpers.field_mask(None, ad_group._pb))
    service.mutate_ad_groups(customer_id=CUSTOMER_ID, operations=[operation])


def lambda_handler(event, context):
    failures = []
    for record in event["Records"]:
        try:
            if record["messageAttributes"]["topic"]["stringValue"] != "inventory_levels/update":
                continue
            level = json.loads(record["body"])
            item_id = str(level["inventory_item_id"])
            watched = WATCHED.get(item_id)
            if watched is None:
                continue
            updated = int(datetime.fromisoformat(level["updated_at"]).timestamp())
            if not is_newest(item_id, updated):
                continue
            if level["available"] is None:  # Bestand wird für diesen Artikel nicht geführt
                continue
            set_ad_group_paused(watched["ad_group_id"], level["available"] <= watched["pause_at"])
        except Exception:
            failures.append({"itemIdentifier": record["messageId"]})
    return {"batchItemFailures": failures}

Der Code zum Pausieren folgt Googles eigenem Beispiel für das Pausieren einer Anzeige, angewendet auf eine Anzeigengruppe.15 Die Liste batchItemFailures ist die teilweise Batch Antwort von Lambda: Ist ReportBatchItemFailures eingeschaltet, gehen nur die gescheiterten Nachrichten zurück in die Warteschlange.16 Der Schutz lässt einen gleichen Zeitstempel absichtlich durch, damit die Wiederholung einer Nachricht, deren Aufruf bei Google gescheitert ist, nicht für eine veraltete gehalten wird. Ein schlichtes Duplikat kommt dadurch auch durch, und das schadet nicht, weil zweimal denselben Status zu setzen nichts ändert.

Rechenbeispiel: die Nachlieferung, die zuerst ankam

Am Donnerstag verkauft Jonas um 16:10 Uhr die letzten zwei Winterzauber Kerzen, und um 16:12 Uhr wird eine Lieferung von 12 Stück eingebucht. Shopify schickt zwei Webhooks: "available: 0, aktualisiert 16:10 Uhr" und "available: 12, aktualisiert 16:12 Uhr". Der von 16:12 Uhr kommt zuerst an.

Der Worker sieht 16:12 Uhr, findet nichts gespeichert, schreibt 16:12 Uhr und lässt die Anzeigengruppe an. Dann trifft die Nachricht von 16:10 Uhr ein. Die Bedingung "zuletzt gesehen ist nicht später als 16:10 Uhr" ist falsch, weil 16:12 Uhr gespeichert ist, also verweigert DynamoDB das Schreiben, und der Worker überspringt die Nachricht. Ohne den Schutz wäre die Anzeigengruppe pausiert worden, während 12 Kerzen im Regal stehen. Kommt die Nachricht von 16:12 Uhr ein zweites Mal, geht sie durch und setzt erneut "an", was nichts ändert.

Wenn ein Aufruf scheitert, warte jedes Mal länger und gib dann laut auf

Manchmal ist die API von Google überlastet. Sofort neu zu versuchen scheitert meistens wieder, und wenn alle Worker gleichzeitig wiederholen, machen sie das Gedränge schlimmer. Die übliche Antwort, wie Marc Brooker sie in der Amazon Builders' Library beschreibt, ist ein gedeckeltes exponentielles Backoff: nach jedem Versuch länger warten, bis zu einer Obergrenze, plus ein bisschen Zufall, Jitter genannt, damit sich die Wiederholungen verteilen. Er warnt auch, dass Wiederholen nur bei idempotenten Aufrufen sicher ist.17

wk=min⁡(wmax⁡,  b⋅2k−1)w_k = \min\left(w_{\max},\; b \cdot 2^{k-1}\right)

Dabei ist wkw_k die Wartezeit vor Wiederholung Nummer kk, bb die Grundwartezeit, 2k−12^{k-1} verdoppelt sie jedes Mal, und wmax⁡w_{\max} ist die Obergrenze. Verdoppeln ist Wachstum um 100 % pro Schritt, die Rechnung aus Grundlagen: Zinseszins. Addierst du die ersten nn Wartezeiten, bevor die Obergrenze greift, ergibt sich die gesamte Wartezeit:

W=b (2n−1)W = b\,(2^{n} - 1)

Dabei ist WW die gesamte Wartezeit über nn Wiederholungen und bb die Grundwartezeit, eine geometrische Summe, erklärt in Grundlagen: Zinseszins.

Rechenbeispiel: vier Wiederholungen im Worker

Der Worker von Jonas wiederholt einen gescheiterten Aufruf bei Google Ads viermal, beginnend bei zwei Sekunden, mit einer Obergrenze von 30 Sekunden, die nie erreicht wird. Die Wartezeiten sind 2, 4, 8 und 16 Sekunden:

W=2×(24−1)=30W = 2 \times (2^4 - 1) = 30

Dabei ist 22 die Grundwartezeit bb in Sekunden, 44 die Zahl der Wiederholungen nn und 3030 die gesamte Wartezeit in Sekunden. Rechnet man die Aufrufe selbst dazu, kann der Worker im schlechten Fall etwa eine Minute brauchen, also bekommt er ein Timeout von 90 Sekunden.

Diese Zahl bestimmt dann die Warteschlange. AWS verlangt ein Sichtbarkeits Timeout der Warteschlange (wie lange eine Nachricht verborgen bleibt, während ein Worker sie bearbeitet) von mindestens dem Sechsfachen des Funktions Timeouts, hier also neun Minuten. Und es empfiehlt, jede Nachricht mindestens fünfmal empfangen zu lassen, bevor sie in eine Dead Letter Queue wandert.18 Eine Dead Letter Queue hält Nachrichten für einen Menschen bereit, wenn jeder Versuch gescheitert ist.19 Jonas bekommt eine Mail, sobald dort etwas landet, denn eine Nachricht, die fünfmal scheitert, ist ein Fehler im Code, und Fehler sollen laut sein.

Nimm für Produkte den Feed und die Anzeigen API nur für das, was der Feed nicht kann

In der alten Fassung dieses Artikels legte ein Webhook für jedes neue Produkt eine Suchanzeige bei Google, eine Anzeigengruppe bei Meta und eine Keyword Gruppe bei Microsoft Ads an. Das würde ich nicht bauen.

Produkte und Bestand gelangen über den Produktfeed im Merchant Center zu Google. Markierst du dort ein Produkt als nicht auf Lager, wird es in den meisten Einkaufsumgebungen nicht mehr angezeigt, und wenn du ein Produkt pausierst, verschwindet es bis zu 14 Tage lang aus Shopping Anzeigen und kostenlosen Produkteinträgen.20 Zu viele Produkte mit falscher Verfügbarkeit können zur Sperrung des Kontos führen.21 Falls dein Shop den Feed per API aktualisiert: Die Content API for Shopping wurde am 18. August 2026 abgeschaltet, Anfragen ohne Verlängerung scheitern seit dem 1. September 2026 zeitweise, und Google rät zum Umstieg auf die Merchant API.22

Was ein Feed nicht kann, ist eine Suchanzeigengruppe zu pausieren, die um ein einzelnes Produkt herum geschrieben ist, wie die Winterzauber Gruppe von Jonas. Genau da verdient der Webhook sein Geld.

Rechenbeispiel: drei neue Kerzen im November

Jonas nimmt im November drei neue Düfte ins Sortiment. Der alte Ansatz hätte drei Suchanzeigen aus einer Vorlage erzeugt, live, bevor sie jemand gelesen hat. Stattdessen kommen die Produkte über den Feed ins Merchant Center, und der Worker ignoriert sie, weil keins davon auf der beobachteten Liste steht. Läuft eins richtig gut, schreibt Jonas ihm eine ordentliche Anzeigengruppe und ergänzt die Liste um eine Zeile. Ein Mensch schreibt die Worte, die Automation übernimmt das Schalten.

Abgebrochene Checkouts: erst die Erlaubnis prüfen, dann die Marge entscheiden lassen

Eine Kundin tippt ihre E Mail Adresse in den Checkout und geht dann. Eine Erinnerung, eine Erinnerung mit Rabatt oder gar nichts?

Die Erlaubnis kommt zuerst. Eine Erinnerung an einen Warenkorb ist Werbung per E Mail, und in Deutschland entscheidet darüber das Gesetz gegen den unlauteren Wettbewerb (UWG). Nach § 7 Abs. 2 Nr. 2 ist Werbung per elektronischer Post ohne vorherige ausdrückliche Einwilligung eine unzumutbare Belästigung. Die einzige Ausnahme ist die für Bestandskunden in § 7 Abs. 3, und sie verlangt unter anderem, dass du die Adresse im Zusammenhang mit dem Verkauf einer Ware oder Dienstleistung bekommen hast und dass du schon bei der Erhebung der Adresse klar und deutlich darauf hinweist, dass die Kundin jederzeit widersprechen kann, nicht erst später in einer Bestellbestätigung.23 Ausführlich habe ich das im Artikel über Einwilligung und E Mail Capture beschrieben. Der Europäische Gerichtshof hat 2025 im Urteil Inteligo Media festgehalten, dass ein Verkauf eine Vereinbarung ist, die notwendig die Zahlung eines Entgelts mit sich bringt, und dass die Ausnahme an eine bestehende Kundenbeziehung anknüpft.24 Zu abgebrochenen Checkouts sagt weder das UWG noch der Gerichtshof etwas. Wer vor dem Bezahlen aufgehört hat, hat aber keine solche Vereinbarung geschlossen, und deshalb würde ich mich bei einer neuen Kundin nicht auf die Ausnahme verlassen. Der sichere Test ist das Häkchen, das die Kundin gesetzt hat: Der Checkout Webhook von Shopify liefert buyer_accepts_marketing neben der E Mail Adresse mit.1 Shopifys eigene Automation für abgebrochene Checkouts schickt standardmäßig nur an Kunden, die Marketing abonniert haben.25 Und wenn du deine eigene App baust, schwärzt Shopify Kundenfelder wie die E Mail Adresse, solange deine App nicht für deren Empfang freigegeben ist.26 Ich bin Ingenieur, kein Anwalt, also würde ich für alles darüber hinaus einen fragen.

Dann die Entscheidung. Die alte Fassung hat einen Rabatt gemailt, sobald die Marge über 40 % lag. Eine feste Schwelle übersieht, wie viele Leute ohnehin zurückgekommen wären und wie viele ein Rabatt zusätzlich bringt. Der Erwartungswert berücksichtigt beides.

ΔE=pa (m−c)−p0 m\Delta E = p_a\,(m - c) - p_0\, m

Dabei ist ΔE\Delta E die erwartete zusätzliche Marge pro abgebrochenem Checkout, wenn du die Aktion ausführst, verglichen mit Nichtstun, pap_a der Anteil, der die Bestellung nach der Aktion abschließt, mm die Marge auf die Bestellung in Euro, cc die Kosten der Aktion pro abgeschlossener Bestellung (der Rabatt) und p0p_0 der Anteil, der ganz ohne Mail zurückgekommen wäre. Der Erwartungswert ist erklärt in Grundlagen: Erwartungswert, und mehr zum Entscheiden auf diese Weise steht in Entscheidungstheorie für Gründer.

Rechenbeispiel: zwei Warenkörbe, zwei Antworten

Aus seinen bisherigen Mails schätzt Jonas, dass 4 % der abgebrochenen Checkouts ohne Mail zurückkommen, 8 % nach einer einfachen Erinnerung und 11 % nach einer Erinnerung mit 10 % Rabatt. Nimm einen Warenkorb über 40 € mit seinen eigenen Kerzen, mit 55 % Marge, also m=22m = 22, und ein Rabatt von 10 % kostet c=4c = 4:

ΔEeinfach=0,08×22−0,04×22=1,76−0,88=0,88\Delta E_{\text{einfach}} = 0{,}08 \times 22 - 0{,}04 \times 22 = 1{,}76 - 0{,}88 = 0{,}88

Dabei ist die einfache Erinnerung 0,880{,}88 Euro pro abgebrochenem Checkout wert. Mit Rabatt:

ΔERabatt=0,11×(22−4)−0,04×22=1,98−0,88=1,10\Delta E_{\text{Rabatt}} = 0{,}11 \times (22 - 4) - 0{,}04 \times 22 = 1{,}98 - 0{,}88 = 1{,}10

Dabei sind 0,080{,}08 und 0,110{,}11 die Rückkehrquoten pap_a, 0,040{,}04 ist p0p_0, 2222 die Marge mm und 44 der Rabatt cc, alles in Euro. Der Rabatt gewinnt mit 22 Cent pro Warenkorb.

Jetzt ein Warenkorb über 40 € mit Geschenksets, die Jonas zukauft und weiterverkauft, mit 25 % Marge, also m=10m = 10, und der Rabatt kostet weiter c=4c = 4:

ΔERabatt=0,11×(10−4)−0,04×10=0,66−0,40=0,26\Delta E_{\text{Rabatt}} = 0{,}11 \times (10 - 4) - 0{,}04 \times 10 = 0{,}66 - 0{,}40 = 0{,}26

Dabei ist 1010 die Marge und 44 der Rabatt. Die einfache Erinnerung bringt 0,08×10−0,40=0,400{,}08 \times 10 - 0{,}40 = 0{,}40, bei diesem Warenkorb gewinnt also die einfache Erinnerung.

Erwartete zusätzliche Marge pro abgebrochenem CheckoutBei einem Warenkorb über 40 € mit 55 % Marge bringt eine Erinnerung mit Rabatt 1,10 € gegenüber 0,88 € für eine einfache. Bei 25 % Marge bringt die einfache Erinnerung 0,40 € gegenüber 0,26 € für die mit Rabatt.Elbkerzen, Warenkorb über 40 €, erwartete zusätzliche Marge je Aktion0,00 €0,50 €1,00 €1,50 €2,00 €0,88 €55 % Marge,einfach1,10 €55 % Marge,10 % Rabatt0,40 €25 % Marge,einfach0,26 €25 % Marge,10 % RabattErwartete zusätzliche Marge pro Checkout (€)

Jeder Balken ist eine Aktion bei einer Art von Warenkorb. Die senkrechte Achse zeigt die erwartete zusätzliche Marge pro abgebrochenem Checkout, verglichen damit, gar nichts zu schicken. Die Erkenntnis: Derselbe Rabatt ist beim einen Warenkorb richtig und beim anderen falsch, wegen der Marge, die er auffrisst, nicht wegen einer magischen Grenze. Die Rückkehrquoten von Jonas sind ausgedacht. Deine kommen aus Tests, und nach meiner Erfahrung verschieben sie sich, weil Kunden, die lernen, dass Abbrechen einen Rabatt bringt, irgendwann mit Absicht abbrechen.

Hier zählt die Deduplizierung, denn eine Mail ist nicht idempotent. Der Code setzt das Worker Modul fort. Die Tabelle claims enthält eine Zeile pro Checkout Token, mit einer Lebensdauer, damit alte Zeilen sich selbst löschen.27 send_reminder steht für den Aufruf, den die API deines E Mail Tools braucht.

import time

P_BASE, P_PLAIN, P_DISCOUNT, DISCOUNT_RATE = 0.04, 0.08, 0.11, 0.10


def claim(key):
    try:
        dynamodb.put_item(
            TableName="claims",
            Item={"key": {"S": key}, "expires_at": {"N": str(int(time.time()) + 30 * 86400)}},
            ConditionExpression="attribute_not_exists(#k)",
            ExpressionAttributeNames={"#k": "key"},
        )
        return True
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            return False
        raise


def handle_abandoned(checkout, margin_rate):
    if checkout.get("completed_at") or not checkout.get("buyer_accepts_marketing"):
        return
    basket = float(checkout["total_price"])
    m, c = basket * margin_rate, basket * DISCOUNT_RATE
    plain = P_PLAIN * m - P_BASE * m
    discount = P_DISCOUNT * (m - c) - P_BASE * m
    key = f"reminder:{checkout['token']}"
    if not claim(key):
        return
    try:
        send_reminder(checkout["email"], checkout["abandoned_checkout_url"],
                      with_discount=discount > plain)
    except Exception:
        dynamodb.delete_item(TableName="claims", Key={"key": {"S": key}})
        raise

Der Eintrag wird geschrieben, bevor die Mail rausgeht, und gelöscht, wenn das Senden scheitert, damit eine Wiederholung es noch einmal versuchen kann. Stirbt die Funktion mitten im Senden, bleibt der Eintrag stehen, und niemand bekommt eine zweite Mail. Mir ist eine fehlende Erinnerung lieber als zwei, und das ist eine Entscheidung, die du bewusst treffen solltest. Ich lasse das eine Stunde nach dem letzten checkouts/update für einen Token laufen und prüfe completed_at dann noch einmal, weil viele Leute von selbst fertig kaufen. Die alte Fassung hat Warenkörbe mit niedriger Marge stattdessen in eine Remarketing Zielgruppe geschoben. Dafür brauchst du eine eigene Erlaubnis, dazu mehr im Artikel über Data Warehousing.

Manche Ereignisse passieren nie: Zeit und Abgleich brauchen einen Zeitplan

Eine Rabattaktion, die um Mitternacht endet, schickt keinen Webhook. Im Shop hat sich nichts geändert, nur die Uhr ist weitergelaufen. In der alten Fassung "hörte" ein Cloudflare Worker auf Rabattkennzeichen, aber ein Worker hört auf gar nichts. Er läuft, wenn eine Anfrage eintrifft oder wenn ein Cron Trigger ihn auslöst.28

Auf AWS übernimmt das der EventBridge Scheduler. Er führt einen einmaligen Zeitplan zu einem festen Datum und einer festen Uhrzeit aus oder einen Cron Zeitplan, jeweils in einer Zeitzone, die du angibst, und stellt sich von selbst auf die Sommerzeit um.29 Die Cron Trigger von Cloudflare laufen nur in UTC.28 Für einen deutschen Shop macht das im Winter eine Stunde aus, zur Mitteleuropäischen Zeit (MEZ), und im Sommer zwei, zur Mitteleuropäischen Sommerzeit (MESZ).

Derselbe Scheduler kümmert sich um die Nachricht, die nie ankommt, über den Abgleichsjob, den Shopify empfiehlt.12 Der von Jonas läuft um 3:00 Uhr, liest den Bestand jedes beobachteten Artikels mit Shopifys eigenem Änderungszeitpunkt und schickt jeden durch denselben Worker. Weil der Worker ältere Nachrichten ignoriert und das Setzen eines Status idempotent ist, kann dieser Durchlauf nie etwas verschlimmern.

Rechenbeispiel: die Frühlingsaktion, die zwei Stunden zu spät endete

Die Frühlingsaktion von Jonas endet um Mitternacht am Ende von Sonntag, dem 31. Mai 2026, deutscher Zeit. Dann gilt die Sommerzeit, und Deutschland ist UTC zwei Stunden voraus. Ein Cloudflare Cron Trigger mit 0 0 1 6 * feuert um Mitternacht UTC, in Hamburg ist es dann 2:00 Uhr. Die Anzeigen mit "20 % Rabatt" laufen also zwei Stunden über das Ende der Aktion hinaus, und wer in dieser Zeit klickt, findet volle Preise.

Mit dem EventBridge Scheduler schreibt Jonas at(2026-06-01T00:00:00) mit der Zeitzone Europe/Berlin, und der Zeitplan feuert um Mitternacht in Hamburg. Auf Cloudflare bräuchte er 0 22 31 5 *, und für eine Aktion im Dezember, wenn die Winterzeit gilt, eine andere Regel mit 23 statt 22 Uhr.

Was du zuerst baust, und wann du das alles gar nicht brauchst

Wenn die eingebauten Automationen deiner Plattform die Aufgabe schon erledigen, nimm sie. Eine Webhook Kette lohnt sich, wenn du eine Regel brauchst, die sie nicht abbilden können: diese Anzeigengruppe unter fünf Stück Bestand pausieren, den Rabatt nach der Marge entscheiden, die Anzeigen mit der Aktion beenden.

Wenn du deinen eigenen Shop mit Ruby on Rails oder Solidus betreibst, brauchst du vielleicht gar keine Serverless Funktionen. Der Shop hat schon einen Server und Hintergrundjobs, und die liefern dasselbe Muster: schnell annehmen, später arbeiten. Die Regeln zu Duplikaten und Wiederholungen aus dem Artikel über Background Workers gelten Wort für Wort, und wie du das testest, steht in TDD und BDD für APIs.

Ein Beispiel: die Reihenfolge, in der Jonas baut. Zuerst der signierte Empfänger und die Warteschlange. Zweitens das Pausieren von Winterzauber mit dem Schutz gegen falsche Reihenfolge. Drittens die Dead Letter Queue und ihre Benachrichtigung. Viertens der Abgleich um 3:00 Uhr. Erst danach die Entscheidung bei abgebrochenen Checkouts. Jeder Schritt funktioniert für sich, und hört er nach Schritt zwei auf, hat er die 43 € vom Wochenende trotzdem zurück. Für das größere Bild gibt es einen einheitlichen Stack für Campaign Automation, und dafür, wie E Mail und Anzeigen sich gegenseitig auslösen, Ads von E Mail Aktivität triggern und umgekehrt.

Der Webhook ist die Klingel, der Worker macht die Tür auf

Die alte Fassung dieses Artikels hat null Verzögerung und volle Kontrolle versprochen. Webhooks sind schnell, aber sie kommen doppelt, in falscher Reihenfolge oder gar nicht, und Serverless skaliert bis zu einem Kontingent. Was du haben kannst, ist eine Kette, die jedes Mal das Richtige tut: Signatur prüfen, in Sekunden antworten, die Arbeit in eine Warteschlange legen, Aktionen wiederholbar machen, veraltete Nachrichten ignorieren, geduldig wiederholen und jede Nacht nachsehen, was durchgerutscht ist. Nichts davon ist raffiniert. Es ist der Unterschied zwischen einer Automation, die du vergessen kannst, und einer, die deinen Bestseller genau an dem Tag pausiert, an dem die Nachlieferung ankommt.

Footnotes

  1. Shopify, Webhooks topics reference (Header und Beispieldaten für inventory_levels/update und checkouts/update), Shopify Entwicklerdokumentation, 2026, https://shopify.dev/docs/api/webhooks/latest

  2. Amazon Web Services, Lambda Kontingente, AWS Lambda Entwicklerhandbuch, 2026, https://docs.aws.amazon.com/de_de/lambda/latest/dg/gettingstarted-limits.html

  3. Cloudflare, Limits, Cloudflare Workers Dokumentation, 2026, https://developers.cloudflare.com/workers/platform/limits/

  4. Amazon Web Services, AWS Lambda Preise, 2026, https://aws.amazon.com/de/lambda/pricing/

  5. Shopify, Verify webhook deliveries, Shopify Entwicklerdokumentation, 2026, https://shopify.dev/docs/apps/build/webhooks/verify-deliveries

  6. Stripe, Registrieren Sie Stripe Ereignisse in Ihrem Webhook Endpoint, Stripe Dokumentation, 2026, https://docs.stripe.com/webhooks?locale=de-DE

  7. Amazon Web Services, Aufrufen von Lambda Funktions URLs, AWS Lambda Entwicklerhandbuch, 2026, https://docs.aws.amazon.com/de_de/lambda/latest/dg/urls-invocation.html

  8. Cloudflare, Web Crypto, Cloudflare Workers Dokumentation, 2026, https://developers.cloudflare.com/workers/runtime-apis/web-crypto/

  9. Cloudflare, JavaScript APIs, Cloudflare Queues Dokumentation, 2026, https://developers.cloudflare.com/queues/configuration/javascript-apis/

  10. Shopify, Troubleshooting webhooks, Shopify Entwicklerdokumentation, 2026, https://shopify.dev/docs/apps/build/webhooks/troubleshooting-webhooks

  11. Shopify, Ignore duplicate webhooks, Shopify Entwicklerdokumentation, 2026, https://shopify.dev/docs/apps/build/webhooks/ignore-duplicates

  12. Shopify, About webhooks, Shopify Entwicklerdokumentation, 2026, https://shopify.dev/docs/apps/build/webhooks

  13. Amazon Web Services, Amazon SQS Zustellung mindestens einmal (at least once), Amazon SQS Entwicklerhandbuch, 2026, https://docs.aws.amazon.com/de_de/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues-at-least-once-delivery.html

  14. Amazon Web Services, CLI Beispiel für DynamoDB Bedingungsausdrücke, Amazon DynamoDB Entwicklerhandbuch, 2026, https://docs.aws.amazon.com/de_de/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html

  15. Google, pause_ad.py, Google Ads API Clientbibliothek für Python, Beispiele, GitHub, 2026, https://github.com/googleads/google-ads-python/blob/main/examples/basic_operations/pause_ad.py

  16. Amazon Web Services, Behandlung von Fehlern für eine SQS Ereignisquelle in Lambda, AWS Lambda Entwicklerhandbuch, 2026, https://docs.aws.amazon.com/de_de/lambda/latest/dg/services-sqs-errorhandling.html

  17. Marc Brooker, Timeouts, retries, and backoff with jitter, Amazon Builders' Library, AWS, 2019, https://builder.aws.com/content/3EumjoZascWd1oZiEgL8ORlv3qE/timeouts-retries-and-backoff-with-jitter

  18. Amazon Web Services, Erstellen und Konfigurieren einer Amazon SQS Zuordnung von Ereignisquellen, AWS Lambda Entwicklerhandbuch, 2026, https://docs.aws.amazon.com/de_de/lambda/latest/dg/services-sqs-configure.html

  19. Amazon Web Services, Verwenden von Warteschlangen für unzustellbare Nachrichten in Amazon SQS, Amazon SQS Entwicklerhandbuch, 2026, https://docs.aws.amazon.com/de_de/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html

  20. Google, Produkte in Ihrem Merchant Center Konto verwalten, Google Merchant Center Hilfe, 2026, https://support.google.com/merchants/answer/14996874?hl=de

  21. Google, Problemlösung: Nicht übereinstimmende Produktverfügbarkeit, Google Merchant Center Hilfe, 2026, https://support.google.com/merchants/answer/12470049?hl=de

  22. Google, Einstellung und Außerbetriebnahme, Content API for Shopping, Google for Developers, 2026, https://developers.google.com/shopping-content/guides/sunset?hl=de

  23. Bundesministerium der Justiz, Gesetz gegen den unlauteren Wettbewerb (UWG), § 7 Unzumutbare Belästigungen, Abs. 2 Nr. 2 und Abs. 3, Gesetze im Internet, 2026, https://www.gesetze-im-internet.de/uwg_2004/__7.html

  24. Gerichtshof der Europäischen Union, Urteil vom 13. November 2025, C-654/23, Inteligo Media, Rn. 48, 53 und 61, EUR Lex, 2025, https://eur-lex.europa.eu/legal-content/DE/TXT/HTML/?uri=CELEX:62023CJ0654

  25. Shopify, Marketing automation types, Shopify Help Center, 2026, https://help.shopify.com/en/manual/promoting-marketing/create-marketing/marketing-automations/types

  26. Shopify, Work with protected customer data, Shopify Entwicklerdokumentation, 2026, https://shopify.dev/docs/apps/launch/protected-customer-data

  27. Amazon Web Services, Verwenden von Time to Live (TTL) in DynamoDB, Amazon DynamoDB Entwicklerhandbuch, 2026, https://docs.aws.amazon.com/de_de/amazondynamodb/latest/developerguide/TTL.html

  28. Cloudflare, Cron Triggers, Cloudflare Workers Dokumentation, 2026, https://developers.cloudflare.com/workers/configuration/cron-triggers/

  29. Amazon Web Services, Zeitplantypen im EventBridge Scheduler, Amazon EventBridge Scheduler Benutzerhandbuch, 2026, https://docs.aws.amazon.com/de_de/scheduler/latest/UserGuide/schedule-types.html

Wenn deine Anzeigen von Bestand und Aktionen immer noch erst am Montagmorgen erfahren, zeichnen wir in einem kostenlosen Workshop zusammen auf, welche Ereignisse in deinem Shop welche Kampagnen bewegen sollten und was sich überhaupt zu automatisieren lohnt. Die ersten zwei Wochen Arbeit gehen auf meine Rechnung.