lomi.

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 POST avec 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-Signature avant de parser le JSON ou d’appliquer l’auth REST de votre API
  • Répondre 200 / 204 en quelques secondes, puis traiter l’événement de façon asynchrone
  • Dédupliquer sur l’id d’é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êteRôle
Content-Typeapplication/json
X-Lomi-SignatureHMAC-SHA256 (hexadécimal) du corps JSON brut, avec le secret de signature du point de terminaison
X-Lomi-EventMême valeur que la propriété "event" à la racine du JSON
User-AgentLomi-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çu 200 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.

Étapes suivantes

Sur cette page