# Traiter les webhooks
Source: https://docs.lomi.africa/build/reliability/handling-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é.

***

title: 'Traiter les webhooks'
description: '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](/build/reliability).

Comportement opérationnel (nouvelles tentatives, doublons, journaux de livraison) : [Fiabilité des webhooks](/build/reliability/webhook-reliability).

## Ce qu'il vous faut

* URL 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’authentification de votre propre 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)

<Callout type="warn" title="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.
</Callout>

<Callout type="info" title="Secret de signature ≠ clé API">
  Les POST webhook utilisent **`X-Lomi-Signature`** (HMAC du corps brut avec le `whsec_…` de l’URL). 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](#webhook-signing-vs-authorization).
</Callout>

## Résumé de la configuration

### Configurer votre URL

Préparez une URL HTTPS dédiée pour recevoir des requêtes POST avec un corps JSON.

```typescript filename="Configuration Express de base pour les webhooks"
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.

```typescript filename="Fonction de vérification de signature webhook"
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;
  }
}
```

<h2 id="webhook-signing-vs-authorization">Secret de signature et en-tête Authorization (éviter les 401)</h2>

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 de l’URL |
| `X-Lomi-Event`     | Même valeur que la propriété `"event"` à la racine du JSON                                 |
| `X-Lomi-Timestamp` | Instant ISO-8601 de construction de l’enveloppe (même valeur que le `timestamp` du corps)  |
| `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`.

**Fenêtre anti-rejeu (facultative) :** `X-Lomi-Timestamp` est informatif. Le HMAC porte toujours uniquement sur le corps brut, vos vérificateurs existants restent valides. Pour refuser les rejeux trop anciens, parsez l’en-tête en ISO-8601 et ignorez les requêtes de plus de **300 secondes**. N’adoptez pas le schéma `t=…,v1=…` : cela casserait tous les vérificateurs déjà livrés.

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](/api/webhooks) : lomi. enregistre le code HTTP et le corps renvoyé par **votre** URL.

## Traiter les événements

Une fois la signature vérifiée, vous pouvez traiter le corps de l’événement en toute sécurité.

```typescript filename="Traitement des événements webhook"
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 le message, 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](/build/reliability/webhook-reliability)). Le travail lourd passe ensuite dans **votre** file interne.

```typescript filename="Réponse rapide et traitement asynchrone"
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 d’événement :** conservez le champ racine `` `id` `` (UUID). Si déjà traité, ignorez-le.
* **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.

```typescript filename="Exemple de vérification d’idempotence"
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 l’URL 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).

```typescript filename="Exemple de journalisation dans le gestionnaire"
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) ...
}
```

## Événements webhook

Lors de la création ou de la mise à jour d’un webhook, vous souscrivez à des types d’événements précis. lomi. n’envoie que ceux auxquels vous êtes abonné.

| Énumération              | Description                                                                   | Type de charge `data`                       |
| ------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------- |
| `PAYMENT_CREATED`        | Une nouvelle tentative de paiement est lancée.                                | `Transaction`                               |
| `PAYMENT_SUCCEEDED`      | Un paiement ponctuel réussit.                                                 | `Transaction`                               |
| `PAYMENT_FAILED`         | Une tentative de paiement ponctuel échoue.                                    | `Transaction`                               |
| `SUBSCRIPTION_CREATED`   | Un abonnement est créé.                                                       | `Subscription`                              |
| `SUBSCRIPTION_UPDATED`   | Un abonnement a changé (pause, reprise, plan, annulation planifiée, etc.).    | `Subscription` (avec `previous_attributes`) |
| `SUBSCRIPTION_RENEWED`   | Un abonnement est renouvelé avec succès.                                      | `Subscription`                              |
| `SUBSCRIPTION_CANCELLED` | Un abonnement est annulé ou expiré.                                           | `Subscription`                              |
| `REFUND_COMPLETED`       | Un remboursement est traité avec succès (Wave, MTN, cartes, π-SPI et manuel). | `Refund`                                    |
| `REFUND_FAILED`          | Une tentative de remboursement échoue.                                        | `Refund`                                    |
| `REFUND_CREATED`         | Une tentative de remboursement est créée.                                     | `Refund`                                    |
| `PAYOUT_CREATED`         | Un versement est créé (`pending`).                                            | `Payout`                                    |
| `PAYOUT_COMPLETED`       | Un versement passe à `completed`.                                             | `Payout`                                    |
| `PAYOUT_FAILED`          | Un versement passe à `failed`.                                                | `Payout`                                    |
| `DISPUTE_CREATED`        | Un litige carte a été ouvert.                                                 | `Dispute`                                   |
| `DISPUTE_UPDATED`        | Statut ou preuves du litige mis à jour.                                       | `Dispute`                                   |
| `DISPUTE_CLOSED`         | Un litige a atteint un état terminal.                                         | `Dispute`                                   |
| `test.webhook`           | Événement de test envoyé via l’API.                                           | `TestPayload`                               |

<Callout type="info">
  Les **échecs de paiement au renouvellement** émettent **`PAYMENT_FAILED`** sur la transaction concernée. Il n’existe pas de webhook dédié « échec abonnement ». Utilisez **`SUBSCRIPTION_RENEWED`** pour les cycles réussis. Voir [Abonnements](/build/billing/subscriptions) et [Comportement du tunnel: Abonnements](/build/accept/checkout-behavior#subscription-checkout).
</Callout>

### Charge utile `SUBSCRIPTION_UPDATED`

Les changements non terminaux utilisent une enveloppe d’événement structurée : état actuel dans `object`, ce qui a changé dans `previous_attributes`, et `context` optionnel pour le débogage.

```json filename="Exemple de charge (SUBSCRIPTION_UPDATED)"
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "event": "SUBSCRIPTION_UPDATED",
  "timestamp": "2026-06-11T14:30:00.000Z",
  "data": {
    "object": {
      "subscription_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "status": "paused",
      "price_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "cancel_at_period_end": false,
      "next_billing_date": "2026-07-01",
      "plan_name": "Pro Mensuel",
      "billing_interval": "month"
    },
    "previous_attributes": {
      "status": "active"
    },
    "context": {
      "source": "merchant_api",
      "actor": "merchant"
    }
  }
}
```

Réconciliez les droits d’accès à partir de `data.object`. Utilisez `previous_attributes` pour distinguer pause, changement de plan, etc. `context.source` vaut `merchant_api`, `customer_portal` ou `cron`.

Les abonnements et les corps JSON utilisent des noms d’événement en **SCREAMING\_SNAKE\_CASE** (par ex. `PAYMENT_SUCCEEDED`). Certains outils peuvent afficher des alias avec des points (par ex. `payment.succeeded`). Utilisez les valeurs ci-dessus pour configurer vos URLs et comparer le champ `event`.

Pour les relances, l’idempotence et les journaux, voir [Fiabilité des webhooks](/build/reliability/webhook-reliability).

## Tester les webhooks

Vous pouvez tester les endpoints avec l’API, `curl`, le [guide des tests](/build/reliability/testing) ou la CLI.

### Créer un webhook de test

Pointez l’URL vers un récepteur de test comme [Webhook.site](https://webhook.site/) ou un tunnel local ngrok.

```bash filename="Terminal"
curl -X POST "https://api.lomi.africa/webhooks" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "url": "YOUR_TEST_WEBHOOK_URL",
    "authorized_events": ["PAYMENT_SUCCEEDED", "test.webhook"],
    "description": "Test Endpoint"
  }'
```

Notez l’`id` et le `secret` de la réponse.

### Envoyer un événement de test

Utilisez `POST /webhooks/{id}/test` pour envoyer un événement `test.webhook`.

```bash filename="Terminal"
curl -X POST "https://api.lomi.africa/webhooks/YOUR_WEBHOOK_ID/test" \
  -H "X-API-Key: YOUR_API_KEY"
```

Confirmez la réception et vérifiez la signature avec le secret stocké.

## 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 URL et relancer manuellement les livraisons en échec.

<DocsNextSteps>
  <DocsNextStep href="/build/reliability/security-best-practices" hint="Secrets et corps brut">
    Bonnes pratiques de sécurité
  </DocsNextStep>

  <DocsNextStep href="/build/reliability/error-handling" hint="Réessais et échecs">
    Gestion des erreurs
  </DocsNextStep>

  <DocsNextStep href="/api" hint="Requêtes et réponses exactes">
    Référence API
  </DocsNextStep>
</DocsNextSteps>
