Webhooks & Events
Recevez des notifications en temps reel pour tous les evenements bancaires : transactions, onboarding, cartes et consentements. Verifiez les signatures HMAC-SHA256 et assurez l'idempotence avec Redis.
Vue d'ensemble
Lorsqu'un evenement se produit chez Swan (transaction comptabilisee, carte activee, onboarding termine, etc.), un webhook est envoye a l'endpoint YaniPay configure. Le flux complet passe par plusieurs etapes de verification avant le traitement metier.
Chaque etape est critique : la verification HMAC garantit l'authenticite du message, le controle d'idempotence Redis empeche les doublons, et le handler applique la logique metier avant de mettre a jour la base de donnees et d'envoyer les notifications.
Verification HMAC-SHA256
Chaque webhook Swan inclut un header swan-signature contenant un hash HMAC-SHA256 du body. Cette signature permet de verifier que le payload provient bien de Swan et n'a pas ete altere en transit.
Secret webhook critique
SWAN_WEBHOOK_SECRETdans le code source. Cette cle est critique pour la verification de l'authenticite des webhooks. Utilisez uniquement des variables d'environnement.1 import crypto from 'crypto'; 2 3 function verifySwanWebhook( 4 payload: string, 5 signature: string, 6 secret: string 7 ): boolean { 8 const expected = crypto 9 .createHmac('sha256', secret) 10 .update(payload) 11 .digest('hex'); 12 13 return crypto.timingSafeEqual( 14 Buffer.from(signature), 15 Buffer.from(expected) 16 ); 17 } 18 19 // In the webhook handler 20 export async function POST(request: Request) { 21 const body = await request.text(); 22 const signature = request.headers.get('swan-signature') ?? ''; 23 24 if (!verifySwanWebhook(body, signature, process.env.SWAN_WEBHOOK_SECRET!)) { 25 return new Response('Invalid signature', { status: 401 }); 26 } 27 // Process event... 28 }
La fonction crypto.timingSafeEqualest utilisee a la place d'une comparaison classique (===) pour prevenir les attaques par timing. Cette approche garantit que le temps de comparaison est constant, quelle que soit la position du premier caractere different.
Idempotence Redis
Swan peut renvoyer un meme webhook plusieurs fois en cas de timeout ou d'erreur reseau. Pour eviter de traiter un evenement en double, chaque eventId est stocke dans Redis avec un TTL de 7 jours.
Risque de doublons
1 import { redis } from '@/lib/redis'; 2 3 async function isProcessed(eventId: string): Promise<boolean> { 4 const key = `webhook:swan:${eventId}`; 5 const exists = await redis.get(key); 6 if (exists) return true; 7 8 // Mark as processed with 7-day TTL 9 await redis.set(key, '1', 'EX', 604800); 10 return false; 11 }
Le pattern est simple : avant de traiter un evenement, on verifie si son eventId existe deja dans Redis. Si oui, on retourne immediatement un status 200sans traitement. Sinon, on enregistre l'ID et on procede au traitement.
1 export async function POST(request: Request) { 2 // 1. Verify HMAC signature 3 const body = await request.text(); 4 const signature = request.headers.get('swan-signature') ?? ''; 5 6 if (!verifySwanWebhook(body, signature, process.env.SWAN_WEBHOOK_SECRET!)) { 7 return new Response('Invalid signature', { status: 401 }); 8 } 9 10 const event = JSON.parse(body); 11 12 // 2. Check idempotency 13 if (await isProcessed(event.eventId)) { 14 return new Response('Already processed', { status: 200 }); 15 } 16 17 // 3. Process event (async) 18 processEvent(event).catch(console.error); 19 20 // 4. Return 200 immediately 21 return new Response('OK', { status: 200 }); 22 }
Categories d'evenements
Swan emet des evenements regroupes en cinq categories principales. Chaque categorie correspond a un domaine fonctionnel de la plateforme bancaire.
| Categorie | Evenements | Description |
|---|---|---|
Onboarding | onboarding.completedonboarding.rejected | Parcours KYC/KYB termine ou refuse |
Account | account.openedaccount.suspendedaccount.closed | Cycle de vie du compte bancaire |
Transaction | transaction.bookedtransaction.rejectedtransaction.pending | Mouvements financiers (virements, paiements) |
Card | card.activatedcard.lockedcard.canceled | Gestion du cycle de vie des cartes |
Consent | consent.grantedconsent.refusedconsent.expired | Consentements DSP2 et autorisations |
Pattern Matching (ts-pattern)
Pour router chaque evenement vers son handler, YaniPay utilise ts-patternqui offre un pattern matching exhaustif et type-safe. Cette approche est plus maintenable qu'une chaine de if/else ou un switch classique.
1 import { match } from 'ts-pattern'; 2 3 const result = match(event.type) 4 .with('Transaction.Booked', () => handleBookedTransaction(event)) 5 .with('Transaction.Rejected', () => handleRejectedTransaction(event)) 6 .with('Card.Activated', () => handleCardActivation(event)) 7 .with('Onboarding.Completed', () => handleOnboardingComplete(event)) 8 .otherwise(() => { 9 console.log(`Unhandled event type: ${event.type}`); 10 });
L'avantage de ts-patternest qu'il force a gerer tous les cas possibles (ou a definir un otherwiseexplicite). En combinaison avec les types TypeScript, cela previent les oublis lors de l'ajout de nouveaux evenements.
Tester les webhooks
Tester les webhooks en local necessite d'exposer votre serveur de developpement a Internet pour que Swan puisse y envoyer des requetes. Voici les trois approches recommandees.
1. Tunnel avec ngrok
ngrok cree un tunnel securise entre Swan et votre machine locale. Installez-le puis lancez le tunnel sur le port de votre serveur Next.js.
# Installer ngrok
brew install ngrok
# Lancer le tunnel vers votre serveur Next.js
ngrok http 3000
# Copier l'URL HTTPS generee (ex: https://abc123.ngrok.io)
# Configurer cette URL dans Swan Dashboard > Webhooks
# Endpoint: https://abc123.ngrok.io/api/webhooks/swan2. Swan Sandbox
Le dashboard Swan Sandbox permet d'envoyer des evenements de test directement depuis l'interface. Accedez a la section Developers > Webhooks > Send test eventpour simuler n'importe quel type d'evenement.
3. Simulation manuelle avec curl
Vous pouvez aussi simuler un webhook manuellement en generant une signature HMAC-SHA256 valide et en envoyant une requete POST avec curl.
# Generer la signature HMAC-SHA256
PAYLOAD='{"eventId":"evt_test_123","type":"Transaction.Booked","data":{"transactionId":"txn_456","amount":{"value":"42.00","currency":"EUR"}}}'
SECRET="your_webhook_secret_here"
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
# Envoyer le webhook simule
curl -X POST http://localhost:3000/api/webhooks/swan \
-H "Content-Type: application/json" \
-H "swan-signature: $SIGNATURE" \
-d "$PAYLOAD"Reponse rapide obligatoire
Bonnes pratiques
Toujours verifier la signature HMAC
Ne traitez jamais un webhook sans avoir verifie la signature. Utilisez crypto.timingSafeEqual pour prevenir les attaques par timing.
Implementer l'idempotence avec Redis
Stockez chaque eventId dans Redis avec un TTL de 7 jours. Retournez 200 immediatement si l'evenement a deja ete traite.
Repondre 200 immediatement
Retournez un HTTP 200 avant de lancer le traitement asynchrone. Swan considere le webhook en echec apres 5 secondes sans reponse.
Logger tous les evenements
Enregistrez chaque webhook recu (type, eventId, timestamp) dans vos logs pour faciliter le debugging et l'audit.
Gerer les retries gracieusement
Swan retente les webhooks en echec avec un backoff exponentiel. Votre handler doit etre idempotent pour gerer ces retries sans effet de bord.
Utiliser ts-pattern pour le routing
Preferez le pattern matching exhaustif avec ts-pattern plutot qu'une chaine de if/else. Cela garantit que tous les types d'evenements sont geres.