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
WebhookSignatureValidatorreste 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
{
"McmApi": {
"WebhookSecret": "<base64-secret-fourni-par-mcm>",
"MaxTimestampAgeSeconds": 300
}
}
| Clé | Défaut | Note |
|---|---|---|
WebhookSecret | (requis) | Base64. Le SDK le décode pour valider la signature HMAC. |
MaxTimestampAgeSeconds | 300 (5 min) | Fenêtre anti-rejeu. Le timestamp futur est aussi rejeté. |
Wiring
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).
| Constante | Chaîne (EventType) | Type de Data | Déclencheur |
|---|---|---|---|
WebhookEventTypes.AdhesionSignee | adhesion.signee | B2BAdhesionItemV2 | Une 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.ListeElectoraleCree | campagne.liste_electorale_cree | B2BListeElectoraleCreeData | Liste é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 retourne200 OKet 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ête | Validation | Action si invalide |
|---|---|---|
X-MCM-Signature | HMAC-SHA256(secret, "{X-MCM-Timestamp}.{body}"), comparaison constant-time | 401 Unauthorized |
X-MCM-Timestamp | Unix seconds. Différence |now - timestamp| ≤ MaxTimestampAgeSeconds | 401 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 :
| Tentative | Délai depuis l'événement |
|---|---|
| 1 | 0 (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 OKaussi vite que possible (en déléguant le travail à une file de tâches), et ne retournez5xxque 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
- Acquittez vite — déléguez à une file dès que >100 ms de travail.
- Dédoublonnez —
DeliveryId+ table d'idempotence. C'est le seul moyen de tolérer les retries. - Journalisez
EventType,DeliveryId,TimestampUtc— corrélation entre votre côté et MCM. - Ne validez pas la signature à la main si vous utilisez
MapMcmWebhooks— c'est déjà fait. - 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.
- N'exposez pas le chemin à Internet sans le middleware — sans validation de signature vous acceptez n'importe quel POST.
- Vérification de santé du chemin —
GET /webhooks/mcmretournera 405. C'est attendu. - Rotation du secret : maintenez deux secrets en chevauchement le temps que MCM bascule.
Voir aussi
WebhookEventTypes(référence)- Idempotence et retries
- Dépannage / FAQ — section Webhooks
- Conciliation — usage typique du webhook
adhesion.signee