Skip to content
← Alle Beiträge
· Delta1 Labs LizenzierungWebhooksLeitfaden

Lizenzauslieferung mit Fulfillment-Webhooks automatisieren

Eine bezahlte Bestellung sollte eine Lizenz ohne menschliches Zutun erzeugen. So funktionieren Fulfillment-Webhooks von Anfang bis Ende: die Signatur über den rohen Body prüfen, die Auslieferung pro Bestellung idempotent machen und die Ereignisse Bezahlung, Rückerstattung und Stornierung auf Ausstellen, Widerrufen und Verlängern abbilden.

Software zu verkaufen sollte nicht erfordern, dass ein Mensch nach jedem Verkauf einen Lizenzschlüssel in eine E-Mail kopiert. Wenn ein Kunde zahlt, sollte Sekunden später eine Lizenz in seinem Postfach erscheinen, und wenn er zurückerstattet, sollte sie aufhören zu funktionieren — alles automatisch. Der Mechanismus, der das möglich macht, ist der Fulfillment-Webhook: ein Rückruf Ihres Zahlungsanbieters, den Ihr Lizenzsystem in ein Ausstellen, Verlängern oder Widerrufen verwandelt. Richtig gemacht, ist er zuverlässig und unsichtbar. Nachlässig gemacht, ist er ein Weg für jeden, kostenlose Lizenzen zu erzeugen. Der Unterschied sind drei Details.

Der Ablauf

Ein Kunde schließt den Checkout bei Ihrem Zahlungsanbieter ab. Der Anbieter erfasst die Bestellung und sendet einen HTTP-POST an die Webhook-URL, die Sie konfiguriert haben, mit einem JSON-Body, der beschreibt, was passiert ist, und einem Signatur-Header. Ihr Handler prüft die Signatur, liest das Ereignis und handelt: Eine bezahlte Bestellung erzeugt eine Lizenz und mailt den Schlüssel; eine Rückerstattung oder Stornierung widerruft sie. Der Anbieter erwartet eine 2xx-Antwort; erhält er sie nicht, wiederholt er.

Keyright stellt genau diesen Endpunkt bereit — POST /webhooks/fulfill/{tenant} — und ein dünner Adapter normalisiert die Nutzlast jedes Anbieters auf eine einzige Form, sodass es der Lizenzlogik egal ist, ob der Verkauf von Stripe, Paddle, Lemon Squeezy oder Ihrer eigenen Abrechnung kam. Die Zahlungsabwicklung bleibt beim Anbieter; der Webhook steuert nur die Lizenzierung.

Detail 1 — prüfen Sie die Signatur über den rohen Body

Die Webhook-URL ist öffentlich. Wenn Ihr Handler dem vertraut, was er empfängt, kann ein Angreifer, der die URL errät, {"type":"paid", ...} posten und sich eine Lizenz ausstellen. Die Verteidigung ist ein gemeinsames Geheimnis: Der Anbieter berechnet einen HMAC des rohen Anfrage-Body mit einem Geheimnis, das nur Sie und er kennen, und sendet ihn in einem Header (Keyright nutzt X-Keyright-Signature). Sie berechnen ihn neu und vergleichen.

Die Feinheit, über die man stolpert: Prüfen Sie über die exakten rohen Bytes, die Sie empfangen haben, vor dem Deserialisieren. Wenn Sie das JSON parsen und zum Hashen neu serialisieren, ändern sich Leerzeichen und Schlüsselreihenfolge, und die Signatur wird nie passen.

app.MapPost("/webhooks/fulfill/{tenant}", async (HttpContext ctx, string tenant) =>
{
    // Read the RAW body first — do not bind to a model yet.
    ctx.Request.EnableBuffering();
    using var reader = new StreamReader(ctx.Request.Body);
    string rawBody = await reader.ReadToEndAsync();

    string provided = ctx.Request.Headers["X-Keyright-Signature"];
    string expected = Convert.ToHexString(
        HMACSHA256.HashData(Secret(tenant), Encoding.UTF8.GetBytes(rawBody))
    ).ToLowerInvariant();

    // Constant-time compare so a timing side channel can't leak the secret.
    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.ASCII.GetBytes(provided ?? ""), Encoding.ASCII.GetBytes(expected)))
        return Results.Unauthorized();

    var evt = JsonSerializer.Deserialize<FulfillmentEvent>(rawBody)!;
    // ... dispatch, below
});

Zwei Dinge zum Übernehmen: Nutzen Sie FixedTimeEquals statt ==, damit der Vergleich das Geheimnis nicht über einen Timing-Seitenkanal preisgibt, und halten Sie das Geheimnis pro Tenant, damit ein geleaktes Geheimnis nicht Bestellungen für alle fälschen kann.

Detail 2 — machen Sie es idempotent

Netzwerke versagen und Anbieter wiederholen. Dasselbe „Bestellung bezahlt”-Ereignis wird früher oder später zweimal ankommen: ein Timeout auf Ihrer Seite, eine Störung auf deren, ein manueller erneuter Versand aus deren Dashboard. Wenn jede Zustellung eine Lizenz erzeugt, wird aus einem Verkauf drei Schlüssel.

Die Lösung ist, das Fulfillment idempotent zu machen, mit einem stabilen Bezeichner als Schlüssel, den der Anbieter bei jedem Wiederholungsversuch mitschickt: der Bestell- oder Abonnementreferenz. Stellen Sie die Lizenz mit dieser Referenz markiert aus und suchen Sie sie zuerst:

async Task<License> Fulfill(FulfillmentEvent evt)
{
    // Same order reference on every retry → find-or-create, never duplicate.
    var existing = await _licenses.FindByOrderRef(evt.OrderRef);
    if (existing is not null) return existing;

    return await _licenses.Issue(new IssueRequest
    {
        Product  = evt.Product,
        Tier     = evt.Tier,
        Email    = evt.CustomerEmail,
        OrderRef = evt.OrderRef,          // the idempotency key
        Seats    = evt.Quantity,
    });
}

Keyright tut das intern — die Ausstellung ist pro Bestellreferenz idempotent — aber das Prinzip gilt überall, wo Sie es bauen: Ein Webhook-Handler muss sicher mehrfach mit identischer Eingabe aufrufbar sein und dasselbe Ergebnis liefern.

Detail 3 — bilden Sie Ereignisse auf Lizenzaktionen ab

Ein Zahlungsanbieter sendet mehr als „bezahlt”. Entscheiden Sie einmal, was jedes Ereignis für eine Lizenz bedeutet, und halten Sie die Abbildung klein und explizit:

Task Handle(FulfillmentEvent evt) => evt.Type switch
{
    "paid" or "subscription.renewed" => Fulfill(evt),            // issue or extend
    "refunded" or "subscription.cancelled" => Revoke(evt.OrderRef), // stop it working
    "payment.failed"                 => FlagPastDue(evt.OrderRef),  // optional grace
    _                                => Task.CompletedTask,       // ignore the rest
};

Eine Verlängerung erweitert die bestehende Lizenz, statt einen neuen Schlüssel zu erzeugen — der Kunde behält den Schlüssel, den er bereits eingebettet hat. Eine Rückerstattung oder Rückbuchung widerruft sie, was sie (wie in früheren Beiträgen behandelt) beim nächsten Online-Check stoppt und, für Offline-Clients, über eine signierte Widerrufsliste. Eine fehlgeschlagene Zahlung ist der Punkt, an dem Sie die Richtlinie wählen: sofort anhalten oder als überfällig markieren und ein Kulanzfenster vor dem Widerruf erlauben.

CheckoutWebhook POSTVerify HMACDedupe by refIssue +email key

Antworten Sie schnell, scheitern Sie sicher

Zwei operative Gewohnheiten halten die Integration gesund. Erstens: Antworten Sie schnell. Machen Sie die Signaturprüfung und die Ausstellung synchron, wenn sie schnell sind, aber wenn ein Schritt langsam ist, bestätigen Sie mit einem 2xx und erledigen Sie die Arbeit im Hintergrund — ein Anbieter, der zu lange wartet, markiert die Zustellung als fehlgeschlagen und wiederholt, was die Last verstärkt. Zweitens: Behandeln Sie eine fehlende oder ungültige Signatur als harte Ablehnung (401), nie als stillen Erfolg; und protokollieren Sie jede abgelehnte Zustellung, denn ein Schwall davon ist entweder ein falsch konfiguriertes Geheimnis oder jemand, der Ihren Endpunkt abtastet.

Verdrahten Sie diese drei Details — über den rohen Body prüfen, per Bestellreferenz deduplizieren, Ereignisse explizit abbilden — und die Lizenzauslieferung wird zu etwas, woran Sie nie wieder denken: Ein Kunde zahlt, und sein Schlüssel wartet auf ihn, bevor er den Tab wechselt.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.