Traiter les webhooks
Les webhooks fournissent des mises à jour en temps réel sur les événements de votre compte lomi.. Ce guide explique comment recevoir et traiter ces notifications en toute sécurité.
Pour une introduction générale et la configuration, voir Configurer les webhooks.
Comportement opérationnel (nouvelles tentatives, doublons, journaux de livraison) : Fiabilité des webhooks.
Liste de contrôle
- Point de terminaison HTTPS acceptant
POSTavec corps JSON - Secret de signature webhook (
whsec_…) du tableau de bord —LOMI_WEBHOOK_SECRET, non envoyé par lomi. à la livraison - Middleware conservant le corps brut pour le HMAC (voir ci-dessous)
- Vérifier
X-Lomi-Signatureavant de parser le JSON ou d’appliquer l’auth REST de votre API - Répondre
200/204en quelques secondes, puis traiter l’événement de façon asynchrone - Dédupliquer sur l’
idd’événement (les doublons sont normaux)
Corps brut : vérifier avant de parser
La signature doit utiliser les octets exacts envoyés par lomi. Re-sérialiser le JSON après express.json() casse le HMAC.
Incorrect : app.post('/webhook', express.json(), …) puis JSON.stringify(req.body) pour le MAC.
Correct : app.post('/webhook', express.raw({ type: 'application/json' }), …) puis passer req.body (Buffer) au vérificateur, et JSON.parse seulement après validation.
Secret de signature ≠ clé API
Les POST webhook utilisent X-Lomi-Signature (HMAC du corps brut avec le whsec_… du point de terminaison). lomi. n’envoie pas X-API-Key ni Authorization: Bearer. Un 401 dans les journaux vient souvent de votre middleware — voir Secret de signature vs Authorization.
Résumé de la configuration
Configurer votre point de terminaison
Préparez un point de terminaison HTTPS dédié pour recevoir des requêtes POST avec un corps JSON.
import express from 'express';
import crypto from 'crypto';
const app = express();
// Définir la fonction de traitement du webhook
async function handleWebhook(req: express.Request, res: express.Response) {
const LOMI_WEBHOOK_SECRET = process.env.LOMI_WEBHOOK_SECRET;
if (!LOMI_WEBHOOK_SECRET) {
console.error('Webhook secret is not configured.');
return res.status(500).send('Webhook configuration error');
}
// Vérifier la signature (implémentation ci-dessous)
const signature = req.headers['x-lomi-signature'] as string;
if (
!signature ||
!verifySignature(req.body, signature, LOMI_WEBHOOK_SECRET)
) {
return res.status(400).send('Invalid signature');
}
// Répondre rapidement pour accuser réception
res.status(200).json({ received: true });
// Traiter l’événement de manière asynchrone
const event = JSON.parse(req.body.toString());
try {
await processWebhookEvent(event);
} catch (error) {
console.error('Error processing webhook event:', error);
// Journaliser l’erreur sans faire échouer la réponse à lomi.
}
}
// Utiliser express.raw() pour accéder au corps brut (vérification de signature)
app.post(
'/your-webhook-endpoint',
express.raw({ type: 'application/json' }),
handleWebhook,
);
// Fonction de vérification de signature (voir ci-dessous)
function verifySignature(
payload: Buffer,
signature: string,
secret: string,
): boolean {
// ... implementation ...
return true; // Placeholder
}
// Logique de traitement de l’événement
async function processWebhookEvent(event: any): Promise<void> {
console.log(`Processing event: ${event.id}, Type: ${event.event}`);
// Ajouter votre logique métier selon event.event
}
// Démarrer le serveur...Vérifier les signatures
Vérifiez toujours l’en-tête X-Lomi-Signature pour garantir que la requête provient bien de lomi. et n’a pas été altérée.
import crypto from 'crypto';
function verifySignature(
payload: Buffer, // Corps brut (Buffer)
signatureHeader: string,
secret: string,
): boolean {
if (!payload || !signatureHeader || !secret) {
return false;
}
try {
const hmac = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Comparaison à temps constant
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(hmac),
);
} catch (error) {
console.error('Error during signature verification:', error);
return false;
}
}Secret de signature et en-tête Authorization (éviter les 401)
La gestion des webhooks dans l’API utilise votre clé API marchande (X-API-Key) — mais lomi. n’envoie pas cette clé (ni Authorization: Bearer …) lors de la livraison des événements vers votre URL.
Pour les requêtes sortantes (lomi. → votre serveur), attendez-vous surtout à :
| En-tête | Rôle |
|---|---|
Content-Type | application/json |
X-Lomi-Signature | HMAC-SHA256 (hexadécimal) du corps JSON brut, avec le secret de signature du point de terminaison |
X-Lomi-Event | Même valeur que la propriété "event" à la racine du JSON |
User-Agent | Lomi-Webhook/1.0 |
Le secret whsec_… sert à vérifier X-Lomi-Signature sur le corps brut — ce n’est pas un jeton que lomi. place dans Authorization.
Si les journaux de livraison indiquent un HTTP 401, ou un corps du type « Authentication required », la réponse vient presque toujours de votre serveur ou proxy (middleware d’authentification Express/Nest/etc., passerelle API, Cloudflare Access, règle Bearer globale…) avant que votre gestionnaire webhook ne s’exécute.
À faire côté intégration : exposer une route dédiée au webhook (chemin ou sous-domaine) sans middleware global exigeant clé API ou Bearer ; vérifier d’abord X-Lomi-Signature, puis répondre 2xx. Vos autres routes peuvent garder Bearer / clés API comme d’habitude.
Voir aussi Webhooks : lomi. enregistre le code HTTP et le corps renvoyé par votre point de terminaison.
Traiter les événements
Une fois la signature vérifiée, vous pouvez traiter le corps de l’événement en toute sécurité.
interface LomiWebhookEvent {
id: string; // UUID — idempotence / dédoublonnage
event: string; // ex. 'PAYMENT_SUCCEEDED'
timestamp: string;
data: any; // Structure selon le type d’événement
/** Reprend le `NODE_ENV` de l’hôte expéditeur (p. ex. `production`, `development`) — pas un drapeau bac à sable checkout */
lomi_environment: string;
}
async function processWebhookEvent(event: LomiWebhookEvent): Promise<void> {
// Facultatif : vérifier si l’identifiant a déjà été traité (idempotence)
if (await hasEventBeenProcessed(event.id)) {
console.log(`Event ${event.id} already processed. Skipping.`);
return;
}
console.log(`Processing event: ${event.id}, Type: ${event.event}`);
switch (event.event) {
case 'PAYMENT_SUCCEEDED':
const transaction = event.data; // Objet transaction
console.log(
`Payment succeeded for transaction: ${transaction.transaction_id}`,
);
// Ex. : livrer la commande, accorder l’accès, mettre à jour la base
// await fulfillOrder(transaction.metadata?.order_id, transaction);
break;
case 'PAYMENT_FAILED':
const failedTxn = event.data;
console.log(
`Payment failed for transaction: ${failedTxn.transaction_id}`,
);
// Ex. : notifier le client, passer la commande en échec
// await handleFailedPayment(failedTxn.metadata?.order_id, failedTxn);
break;
case 'SUBSCRIPTION_CREATED':
const subscription = event.data;
console.log(`Subscription created: ${subscription.subscription_id}`);
// Ex. : provisionner le service pour l’abonnement
break;
case 'SUBSCRIPTION_RENEWED':
const renewed = event.data;
console.log(`Subscription renewed: ${renewed.subscription_id}`);
// Ex. : prolonger l’accès pour la nouvelle période
break;
case 'SUBSCRIPTION_CANCELLED':
const cancelledSub = event.data;
console.log(`Subscription cancelled: ${cancelledSub.subscription_id}`);
// Ex. : révoquer l’accès immédiatement ou en fin de période
break;
// Ajouter d’autres cas pour les événements auxquels vous êtes abonné…
default:
console.warn(`Unhandled event type: ${event.event}`);
}
// Facultatif : marquer l’événement comme traité
await markEventAsProcessed(event.id);
}
// Fonctions fictives pour l’idempotence (à implémenter avec votre BDD/cache)
async function hasEventBeenProcessed(eventId: string): Promise<boolean> {
// Vérifier dans votre stockage si eventId existe
return false; // Remplacer par la vérification réelle
}
async function markEventAsProcessed(eventId: string): Promise<void> {
// Enregistrer eventId dans votre stockage
}Bonnes pratiques
Répondre rapidement
Accusé‑réception d’abord : voyez le `POST` webhook comme un transport minimal. Validez `X-Lomi-Signature`, conservez ou enfilez l’essentiel pour ne pas perdre la charge utile, puis renvoyez tout de suite un 200 / 204 à lomi. Tout ce qui touche à des tiers, à une base lourde ou à des sagas multi‑étapes doit s’exécuter après que la boucle HTTP a réussi aux yeux de lomi.
Répondez en quelques secondes tout au plus dans les cas extrêmes, mais visez sous une seconde en usage courant : chaque envoi HTTP sortant côté lomi est cadencé par un timeout de lecture d’environ quatre secondes ; le dépasser fait échouer la tentative et peut consommer une partie de vos relances automatiques (matrice détaillée). Le travail lourd passe ensuite dans votre file interne.
async function handleWebhook(req: express.Request, res: express.Response) {
// ... (vérifier la signature) ...
if (!isValidSignature) {
return res.status(400).send('Invalid signature');
}
// Accuser réception tout de suite
res.status(200).json({ received: true });
// Ajouter l’événement à une file en arrière-plan
const event = JSON.parse(req.body.toString());
backgroundQueue.add('process-webhook', event);
}Gérer les doublons (idempotence)
Les problèmes réseau, les relances automatiques, les rejeux manuels depuis le tableau de bord et les garde‑fous d’idempotence côté plateforme font qu’il faut attendre plusieurs POST dans le temps pour un même événement logique : c’est le fonctionnement normal — intégrez la déduplication dès la conception de votre schéma, pas comme bug rare.
- Vérifier l’identifiant de charge : conservez le champ racine
`id`(UUID). Si déjà traité, faites un no-op. - Contraintes en base : utilisez des contraintes d’unicité lorsque c’est pertinent (par ex. sur une mise à jour de commande liée à l’ID de transaction) pour éviter les doublons au niveau données.
async function processWebhookEvent(event: LomiWebhookEvent): Promise<void> {
const isProcessed = await database.checkIfEventProcessed(event.id);
if (isProcessed) {
console.log(`Event ${event.id} is a duplicate, skipping.`);
return;
}
// ... traiter l’événement ...
await database.markEventAsProcessed(event.id);
}Gestion des erreurs
Mettez en place une gestion d’erreurs robuste dans processWebhookEvent.
- Journaliser : tracez les erreurs détaillées pendant le traitement.
- Nouvelle tentative interne : pour les erreurs transitoires (par ex. indisponibilité temporaire de la base), envisagez des réessais dans le worker de la file.
- Surveillance : surveillez les échecs sur le point de terminaison webhook et dans la file de traitement.
- Ne pas faire échouer la réponse
200 OK: même si le traitement interne échoue ensuite, lomi. doit déjà avoir reçu200 OK. Ce qui compte pour lomi., c’est l’accusé de livraison réussi.
Journalisation
Enregistrez les informations utiles au débogage :
- Réception des webhooks (identifiant et type d’événement).
- Résultat de la vérification de signature.
- Début et fin du traitement.
- Erreurs avec le contexte nécessité (évitez de journaliser la charge brute ou des données sensibles sans mesure adaptée).
async function handleWebhook(req: express.Request, res: express.Response) {
const eventId = JSON.parse(req.body.toString())?.id || 'unknown';
console.log(`Received webhook request for event ID (potential): ${eventId}`);
// ... (vérifier la signature) ...
if (!isValidSignature) {
console.warn(`Invalid signature for event ID: ${eventId}`);
return res.status(400).send('Invalid signature');
}
console.log(`Signature verified for event ID: ${eventId}`);
res.status(200).json({ received: true });
// ... (traitement asynchrone) ...
}Tester les webhooks
Voir le guide des tests pour utiliser le Dashboard lomi. ou le CLI et envoyer des événements de test vers votre point de terminaison local.
Surveillance
Utilisez le Dashboard lomi. (Developers → Webhooks) pour suivre les tentatives de livraison, consulter les événements récents, vérifier les codes de réponse de votre point de terminaison et relancer manuellement les livraisons en échec.