Aller au contenu principal

Webhooks

MCM peut pousser des notifications HTTP à votre application au lieu de vous obliger à les interroger périodiquement. Le SDK fournit un middleware ASP.NET prêt à l'emploi : signature HMAC-SHA256, désérialisation typée, dispatch vers vos handlers.

Si vous n'utilisez pas ASP.NET, le validateur WebhookSignatureValidator reste exposé pour valider manuellement.


1. Vue d'ensemble

MCM ──HTTP POST──► votre endpoint /webhooks/mcm


Middleware MCM (signature, timestamp, dispatch)


IMcmWebhookHandler<TData>


Votre logique métier (DB, queue, …)

Garanties :

  • Livraison « au moins une fois » (pas « exactement une fois ») — concevez vos handlers idempotents (dédoublonnez sur DeliveryId).
  • Ordre non garanti entre événements distincts. Deux événements liés peuvent arriver en désordre — fiez-vous aux timestamps métier (DateSignature, etc.).
  • Retries automatiques côté MCM (voir §6).

2. Activation côté MCM

Un administrateur MCM configure votre URL et le sous-ensemble d'événements souhaités, puis génère un secret partagé (base64). Communiquez-le par un canal sécurisé séparé à votre équipe — jamais par courriel non chiffré.

À ce jour, la création/modification d'abonnement webhook n'est pas exposée par le SDK ni par l'API B2B publique. C'est une opération admin côté MCM.


3. Côté serveur (récepteur)

Installation

dotnet add package MCM.ApiProxy
dotnet add package MCM.B2B.Contracts

Configuration

appsettings.json
{
"McmApi": {
"WebhookSecret": "<base64-secret-fourni-par-mcm>",
"MaxTimestampAgeSeconds": 300
}
}
CléDéfautNote
WebhookSecret(requis)Base64. Le SDK le décode pour valider la signature HMAC.
MaxTimestampAgeSeconds300 (5 min)Fenêtre anti-rejeu. Le timestamp futur est aussi rejeté.

Wiring

Program.cs
using MCM.ApiProxy.Webhooks;
using MCM.B2B.Contracts.V2;
using MCM.B2B.Contracts.Webhooks;

builder.Services.AddMcmWebhookReceiver(builder.Configuration)
.AddHandler<AdhesionSigneeHandler, B2BAdhesionItemV2>(WebhookEventTypes.AdhesionSignee)
.AddHandler<ListeElectoraleHandler, B2BListeElectoraleCreeData>(WebhookEventTypes.ListeElectoraleCree);

var app = builder.Build();

app.MapMcmWebhooks("/webhooks/mcm"); // path personnalisable

AddHandler<THandler, TData> enregistre le handler en Scoped — une nouvelle instance par requête. Ses dépendances (DbContext, IMediator, etc.) sont injectables normalement.


4. Écrire un handler

public class AdhesionSigneeHandler(
AppDbContext db,
ILogger<AdhesionSigneeHandler> logger) : IMcmWebhookHandler<B2BAdhesionItemV2>
{
public async Task HandleAsync(
B2BWebhookPayload<B2BAdhesionItemV2> payload,
CancellationToken ct)
{
// 1. Idempotence — dédoublonner par DeliveryId
bool alreadyHandled = await db.WebhookDeliveries
.AnyAsync(d => d.DeliveryId == payload.DeliveryId, ct);

if (alreadyHandled)
{
logger.LogInformation("Delivery {Id} déjà traitée", payload.DeliveryId);
return; // 200 OK — succès sans rejouer
}

// 2. Logique métier
await syndicationService.MarquerAdhesionSignee(payload.Data, ct);

// 3. Marquer comme traité (même transaction si possible)
db.WebhookDeliveries.Add(new WebhookDelivery
{
DeliveryId = payload.DeliveryId,
EventType = payload.EventType,
ReceivedUtc = DateTime.UtcNow
});
await db.SaveChangesAsync(ct);
}
}

Retournez vite. Le timeout côté MCM est court. Si votre traitement est long (>2 s), publiez sur une queue (Hangfire, MassTransit, Azure Service Bus) et acquittez immédiatement.


5. Catalogue d'événements

Les types sont déclarés dans WebhookEventTypes (B2B.Contracts/Webhooks/WebhookEventTypes.cs).

ConstanteChaîne (EventType)Type de DataDéclencheur
WebhookEventTypes.AdhesionSigneeadhesion.signeeB2BAdhesionItemV2Une adhésion vient d'être signée. Inclut le cas conciliation (Confirmer réussi) et signature directe. Les paiements rattachés sont déjà transférés à ce stade.
WebhookEventTypes.ListeElectoraleCreecampagne.liste_electorale_creeB2BListeElectoraleCreeDataListe électorale créée après un ajout en lot de votants sur une campagne. Permet de déclencher le calcul des droits ou la communication aux votants.

D'autres événements seront ajoutés. Surveillez le changelog et les nouvelles entrées de WebhookEventTypes. Si un événement arrive sans handler enregistré, le middleware retourne 200 OK et ignore silencieusement — c'est intentionnel pour préserver la rétro-compatibilité.

Détails du payload

Voir la page Types — webhooks pour la structure complète de chaque DTO.


6. Sécurité

Headers vérifiés par le middleware

En-têteValidationAction si invalide
X-MCM-SignatureHMAC-SHA256(secret, "{X-MCM-Timestamp}.{body}"), comparaison constant-time401 Unauthorized
X-MCM-TimestampUnix seconds. Différence |now - timestamp| ≤ MaxTimestampAgeSeconds401 Unauthorized

Le DeliveryId est lu dans le corps JSON (B2BWebhookPayload.DeliveryId), pas dans les en-têtes. Utilisez-le pour dédoublonner.

Le middleware ne tolère aucun écart de signature. Headers manquants → 401. Body modifié → 401. Timestamp trop vieux ou dans le futur → 401. JSON corrompu → 400.

Validation manuelle (sans middleware)

Si vous routez via une autre stack (Express, Lambda, etc.) :

using MCM.B2B.Contracts.Webhooks;

string body = await new StreamReader(request.Body).ReadToEndAsync();
string signature = request.Headers["X-MCM-Signature"];
string timestamp = request.Headers["X-MCM-Timestamp"];

bool ok = WebhookSignatureValidator.IsValid(secret, timestamp, body, signature);
if (!ok) return Unauthorized();

var envelope = JsonSerializer.Deserialize<B2BWebhookPayload>(body);
// → envelope.EventType, envelope.DeliveryId, envelope.Data (JsonElement)

Protection du secret

  • Jamais en clair dans le code source ou les logs.
  • Stockez-le dans Azure Key Vault, AWS Secrets Manager, ou les secrets utilisateurs en dev.
  • Roulez-le périodiquement — coordonnez avec MCM (rotation sans interruption : MCM accepte deux secrets en parallèle pendant la fenêtre de rotation).

7. Retries et désactivation

Côté MCM, en cas de réponse non-2xx ou de timeout :

TentativeDélai depuis l'événement
10 (live)
2+30 s
3+1 min
4+5 min
5+15 min
6+1 h

Après 3 échecs consécutifs complets (toutes les tentatives échouent pour 3 livraisons distinctes), l'abonnement est automatiquement désactivé et un courriel est envoyé à l'admin. À la réactivation, les événements en file pendant la suspension ne sont pas rejoués.

Côté vous : retournez 200 OK aussi vite que possible (en déléguant le travail à une file de tâches), et ne retournez 5xx que pour les erreurs vraiment transitoires.


8. Idempotence : le piège classique

Le retry au niveau MCM signifie qu'un même événement peut arriver plusieurs fois. Sans table de dédoublonnage, vous risquez :

  • d'envoyer deux courriels de bienvenue,
  • de créer deux lignes de paiement,
  • d'incrémenter deux fois un compteur.

Pattern recommandé :

public async Task HandleAsync(B2BWebhookPayload<B2BAdhesionItemV2> p, CancellationToken ct)
{
// Insertion conditionnelle (UNIQUE INDEX sur DeliveryId)
int rows;
try
{
db.WebhookDeliveries.Add(new() { DeliveryId = p.DeliveryId, EventType = p.EventType });
rows = await db.SaveChangesAsync(ct);
}
catch (DbUpdateException) when (IsUniqueViolation())
{
return; // déjà reçu → 200 OK
}

// Continue uniquement la 1re fois
await DoBusinessLogic(p.Data, ct);
}

Voir le guide idempotence et retries pour l'application aux appels sortants côté SDK.


9. Tester localement

Avec ngrok

ngrok http 5050
# https://abcd-1234.ngrok.io

Donnez l'URL publique à l'admin MCM, ajoutez /webhooks/mcm au path :

https://abcd-1234.ngrok.io/webhooks/mcm

Forger une livraison

Pour tester en isolation sans dépendre de MCM, signez vous-même :

long ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string body = JsonSerializer.Serialize(new B2BWebhookPayload<B2BAdhesionItemV2>
{
EventType = WebhookEventTypes.AdhesionSignee,
TimestampUtc = DateTime.UtcNow,
DeliveryId = Guid.NewGuid(),
Data = new B2BAdhesionItemV2 { /* … */ }
});

using var hmac = new HMACSHA256(Convert.FromBase64String(secret));
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{ts}.{body}"));
string signature = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();

await http.PostAsync("http://localhost:5050/webhooks/mcm",
new StringContent(body, Encoding.UTF8, "application/json")
{
Headers = {
{ "X-MCM-Signature", signature },
{ "X-MCM-Timestamp", ts.ToString() }
}
});

MCM.ApiProxy.Cli propose un menu Webhook listener qui démarre un récepteur local pré-câblé pour explorer les événements (Tests/MCM.ApiProxy.Cli/Menus/WebhookListenerMenu.cs).


10. Bonnes pratiques

  1. Acquittez vite — déléguez à une file dès que >100 ms de travail.
  2. DédoublonnezDeliveryId + table d'idempotence. C'est le seul moyen de tolérer les retries.
  3. Journalisez EventType, DeliveryId, TimestampUtc — corrélation entre votre côté et MCM.
  4. Ne validez pas la signature à la main si vous utilisez MapMcmWebhooks — c'est déjà fait.
  5. Aucun handler ≠ erreur — le middleware tolère les événements non-mappés (200 OK silencieux). Permet à MCM d'ajouter des événements sans casser vos déploiements.
  6. N'exposez pas le chemin à Internet sans le middleware — sans validation de signature vous acceptez n'importe quel POST.
  7. Vérification de santé du cheminGET /webhooks/mcm retournera 405. C'est attendu.
  8. Rotation du secret : maintenez deux secrets en chevauchement le temps que MCM bascule.

Voir aussi