Skip to content
← Tous les articles
· Delta1 Labs LicencesWebhooksGuide

Automatiser la livraison de licences avec des webhooks de fulfillment

Une commande payée devrait générer une licence sans intervention humaine. Voici comment fonctionnent les webhooks de fulfillment de bout en bout : vérifier la signature sur le corps brut, rendre la livraison idempotente par commande, et mapper les événements de paiement, remboursement et annulation vers émettre, révoquer et renouveler.

Vendre un logiciel ne devrait pas exiger qu”un humain copie une clé de licence dans un e-mail après chaque vente. Quand un client paie, une licence devrait apparaître dans sa boîte de réception quelques secondes plus tard, et quand il se fait rembourser, elle devrait cesser de fonctionner — le tout automatiquement. Le mécanisme qui rend cela possible est le webhook de fulfillment : un rappel de votre prestataire de paiement que votre système de licences transforme en émission, renouvellement ou révocation. Bien fait, il est fiable et invisible. Fait avec négligence, c”est un moyen pour n”importe qui de générer des licences gratuites. La différence tient en trois détails.

Le flux

Un client finalise le paiement chez votre prestataire. Le prestataire enregistre la commande et envoie un POST HTTP à l”URL de webhook que vous avez configurée, avec un corps JSON décrivant ce qui s”est passé et un en-tête de signature. Votre gestionnaire vérifie la signature, lit l”événement et agit : une commande payée génère une licence et envoie la clé par e-mail ; un remboursement ou une annulation la révoque. Le prestataire attend une réponse 2xx ; s”il ne l”obtient pas, il réessaie.

Keyright expose exactement cet endpoint — POST /webhooks/fulfill/{tenant} — et un adaptateur mince normalise la charge de chaque prestataire en une seule forme, de sorte que la logique de licence se moque de savoir si la vente vient de Stripe, Paddle, Lemon Squeezy ou de votre propre facturation. Le traitement du paiement reste chez le prestataire ; le webhook ne pilote que les licences.

Détail 1 — vérifiez la signature sur le corps brut

L”URL du webhook est publique. Si votre gestionnaire fait confiance à ce qu”il reçoit, un attaquant qui devine l”URL peut faire un POST de {"type":"paid", ...} et s”émettre une licence. La défense est un secret partagé : le prestataire calcule un HMAC du corps brut de la requête avec un secret que vous seuls connaissez, et l”envoie dans un en-tête (Keyright utilise X-Keyright-Signature). Vous le recalculez et comparez.

La subtilité qui fait trébucher : vérifiez sur les octets bruts exacts que vous avez reçus, avant de désérialiser. Si vous analysez le JSON et le re-sérialisez pour le hacher, les espaces et l”ordre des clés changent et la signature ne correspondra jamais.

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
});

Deux choses à copier : utilisez FixedTimeEquals plutôt que == pour que la comparaison ne fuite pas le secret par un canal temporel, et gardez le secret par tenant pour qu”un secret fuité ne puisse pas falsifier les commandes de tout le monde.

Détail 2 — rendez-le idempotent

Les réseaux échouent et les prestataires réessaient. Le même événement « commande payée » arrivera, tôt ou tard, deux fois : un timeout de votre côté, un aléa du leur, un renvoi manuel depuis leur tableau de bord. Si chaque livraison génère une licence, une vente devient trois clés.

La solution est de rendre le fulfillment idempotent, avec pour clé un identifiant stable que le prestataire inclut à chaque réessai : la référence de commande ou d”abonnement. Émettez la licence étiquetée avec cette référence et cherchez-la d”abord :

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 le fait en interne — l”émission est idempotente par référence de commande — mais le principe tient partout où vous le construisez : un gestionnaire de webhook doit pouvoir être appelé plusieurs fois avec la même entrée et produire le même résultat.

Détail 3 — mappez les événements vers des actions de licence

Un prestataire de paiement émet plus que « payé ». Décidez une fois ce que chaque événement signifie pour une licence, et gardez le mappage petit et explicite :

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
};

Un renouvellement prolonge la licence existante au lieu de générer une nouvelle clé — le client garde la clé qu”il a déjà intégrée. Un remboursement ou une rétrofacturation la révoque, ce qui (comme vu dans des articles précédents) l”arrête à la prochaine vérification en ligne et, pour les clients hors ligne, via une liste de révocation signée. Un paiement échoué est l”endroit où vous choisissez la politique : arrêt immédiat, ou marquage « en souffrance » avec une fenêtre de grâce avant révocation.

CheckoutWebhook POSTVerify HMACDedupe by refIssue +email key

Répondez vite, échouez en sécurité

Deux habitudes opérationnelles maintiennent l”intégration saine. D”abord, répondez vite : faites la vérification de signature et l”émission de façon synchrone si elles sont rapides, mais si une étape est lente, accusez réception avec un 2xx et terminez le travail en arrière-plan — un prestataire qui attend trop longtemps marque la livraison en échec et réessaie, amplifiant la charge. Ensuite, traitez une signature manquante ou invalide comme un rejet dur (401), jamais comme un succès silencieux ; et journalisez chaque livraison rejetée, car une rafale de ces rejets est soit un secret mal configuré, soit quelqu”un qui sonde votre endpoint.

Câblez ces trois détails — vérifier sur le corps brut, dédupliquer par référence de commande, mapper les événements explicitement — et la livraison de licences devient quelque chose auquel vous ne repensez jamais : un client paie, et sa clé l”attend avant même qu”il change d”onglet.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.