> ## Documentation Index
> Fetch the complete documentation index at: https://chariow.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Sécurité des Pulses

> Vérifier qu'un Pulse provient bien de Chariow et dédupliquer les réessais sans risque

Chaque Pulse envoyé par Chariow est signé avec un secret propre à ce Pulse. Vérifier cette signature est le seul moyen de savoir qu'une requête reçue sur votre point de terminaison provient réellement de Chariow : votre URL est publique, donc quiconque la découvre peut y poster.

Cette page constitue le contrat de signature complet. Implémentez-le une fois et vous pouvez faire confiance à chaque charge utile reçue.

## En-têtes de la requête

Chaque livraison porte ces en-têtes :

| En-tête               | Contenu                                                     |
| --------------------- | ----------------------------------------------------------- |
| `x-chariow-signature` | `sha256=<empreinte hexadécimale>` — la signature à vérifier |
| `x-pulse-id`          | L'identifiant du Pulse, ex. `pulse_abc123`                  |
| `x-pulse-delivery-id` | L'identifiant de la livraison — **votre clé d'idempotence** |
| `x-pulse-event`       | Le nom de l'événement, ex. `successful.sale`                |
| `Content-Type`        | `application/json; charset=utf-8`                           |
| `User-Agent`          | `Pulse \| Chariow +https://chariow.com`                     |

<Note>
  Les événements de test envoyés depuis le tableau de bord portent `x-pulse-id` et `x-pulse-event` mais **pas** `x-pulse-delivery-id`, car aucun enregistrement de livraison n'est créé pour eux. Leur charge utile contient également un champ `note` supplémentaire. Les événements réels portent toujours les quatre en-têtes.
</Note>

## Le secret de signature

Chaque Pulse possède son propre secret de signature, préfixé `whsec_`, généré par Chariow à la création du Pulse.

<Warning>
  Le secret de signature n'est **pas** votre clé API, et il n'est dérivé ni de votre clé API, ni de l'identifiant de votre boutique, ni d'aucun autre identifiant d'intégration. C'est une valeur indépendante. Calculer le HMAC avec autre chose ne produira jamais une signature correspondante.
</Warning>

Pour le récupérer : **Automatisations → Pulses → sélectionnez votre Pulse → onglet Aperçu → Secret de signature**. Il est masqué par défaut ; utilisez *Révéler*, *Copier* et *Renouveler* sur ce bloc.

Le secret est chiffré au repos et n'apparaît jamais dans les listings d'API ni dans les journaux de requêtes — ceux-ci n'exposent qu'une forme masquée du type `whsec_••••a1b2`. La valeur en clair n'est renvoyée que sur une demande de révélation explicite.

### Renouveler le secret

Le renouvellement prend effet immédiatement et est irréversible : l'ancien secret cesse de fonctionner à l'instant du renouvellement. Mettez à jour la configuration de votre point de terminaison dans la même fenêtre.

Les livraisons émises avant le renouvellement conservent la signature calculée avec l'ancien secret : adoptez le nouveau secret avant de rejouer d'anciennes livraisons.

## Schéma de signature

| Propriété     | Valeur                                                    |
| ------------- | --------------------------------------------------------- |
| Algorithme    | HMAC-SHA256                                               |
| En-tête       | `x-chariow-signature`                                     |
| Format        | `sha256=<64 caractères hexadécimaux minuscules>`          |
| Donnée signée | Les octets bruts du corps HTTP, exactement tels que reçus |
| Clé           | Le secret de signature du Pulse (`whsec_...`)             |
| Horodatage    | Aucun, par conception                                     |

```
signature = "sha256=" + hex( hmac_sha256( corps_brut_de_la_requete, secret_du_pulse ) )
```

**Seul le corps brut est signé.** La méthode HTTP, l'URL, les en-têtes, `x-pulse-id`, `x-pulse-delivery-id` et tout horodatage sont exclus du calcul.

### Encodage du corps

Le corps est du JSON compact en UTF-8 :

* aucun espace d'indentation ni retour à la ligne ;
* les barres obliques sont échappées — `https:\/\/example.com` ;
* les caractères non-ASCII sont échappés en `\uXXXX`, le corps transmis est donc en pratique de l'ASCII ;
* l'ordre des clés est celui du corps transmis.

<Warning>
  Ne re-sérialisez jamais la charge utile parsée. `JSON.stringify(req.body)` ou `json.dumps(payload)` ne reproduiront pas ces octets — les barres obliques échappées suffisent à casser l'empreinte. Capturez le corps brut **avant** tout parsing JSON.
</Warning>

### Comparer la signature

Retirez le préfixe `sha256=` et comparez à l'empreinte hexadécimale seule, ou reconstruisez `"sha256=" + empreinte` et comparez la chaîne complète. Les deux fonctionnent, à condition d'être cohérent.

Comparez toujours en temps constant (`crypto.timingSafeEqual`, `hash_equals`, `hmac.compare_digest`) et vérifiez d'abord que les deux valeurs ont la même longueur — `timingSafeEqual` lève une exception sur des tampons de tailles différentes.

Le préfixe identifie l'algorithme afin que le schéma puisse évoluer sans casser les intégrations existantes. Traitez toute valeur qui ne commence pas par `sha256=` comme un schéma que vous ne prenez pas encore en charge.

<Warning>
  Rejetez avec un `401` toute requête dépourvue d'un en-tête `x-chariow-signature` valide.
</Warning>

## Exemples de vérification

<CodeGroup>
  ```javascript Node.js / Express theme={null}
  const crypto = require('crypto');

  app.post('/webhooks/chariow',
    express.raw({ type: 'application/json' }),
    (req, res) => {
      const received = req.header('x-chariow-signature') ?? '';
      const expected = 'sha256=' + crypto
        .createHmac('sha256', process.env.CHARIOW_PULSE_SECRET)
        .update(req.body)                 // Buffer brut, jamais un objet re-sérialisé
        .digest('hex');

      const a = Buffer.from(received);
      const b = Buffer.from(expected);

      if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
        return res.status(401).send('Invalid signature');
      }

      const deliveryId = req.header('x-pulse-delivery-id');

      if (alreadyProcessed(deliveryId)) {
        return res.status(200).send('OK');
      }

      res.status(200).send('OK');
      enqueue(deliveryId, JSON.parse(req.body.toString('utf8')));
    }
  );
  ```

  ```php PHP theme={null}
  <?php

  $raw = file_get_contents('php://input');
  $received = $_SERVER['HTTP_X_CHARIOW_SIGNATURE'] ?? '';
  $expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('CHARIOW_PULSE_SECRET'));

  if (! hash_equals($expected, $received)) {
      http_response_code(401);
      exit;
  }

  $deliveryId = $_SERVER['HTTP_X_PULSE_DELIVERY_ID'] ?? null;

  if (already_processed($deliveryId)) {
      http_response_code(200);
      exit;
  }

  $payload = json_decode($raw, true);
  ```

  ```python Python / Flask theme={null}
  import hmac, hashlib, os

  @app.post('/webhooks/chariow')
  def chariow_webhook():
      raw = request.get_data()  # octets bruts, pas request.json
      expected = 'sha256=' + hmac.new(
          os.environ['CHARIOW_PULSE_SECRET'].encode(),
          raw,
          hashlib.sha256,
      ).hexdigest()

      received = request.headers.get('X-Chariow-Signature', '')

      if not hmac.compare_digest(expected, received):
          return '', 401

      delivery_id = request.headers.get('X-Pulse-Delivery-Id')

      if already_processed(delivery_id):
          return '', 200

      enqueue(delivery_id, request.get_json())

      return '', 200
  ```
</CodeGroup>

## Idempotence et protection contre le rejeu

La signature ne contient aucun horodatage, et c'est délibéré. Elle est calculée une seule fois à l'émission de la livraison et réutilisée à l'identique par chaque réessai. Une fenêtre temporelle rejetterait à tort un réessai légitimement retardé — la dernière tentative d'une livraison peut arriver près de trois heures après la première.

La protection contre le rejeu repose sur `x-pulse-delivery-id`. Cet identifiant est stable sur toutes les tentatives d'une même livraison, ce qui en fait une clé d'idempotence directement exploitable :

1. Lisez `x-pulse-delivery-id`.
2. Si vous l'avez déjà traité, renvoyez `200` et arrêtez-vous.
3. Sinon, persistez-le, renvoyez `200`, puis traitez de façon asynchrone.

<Tip>
  Dédupliquez sur `x-pulse-delivery-id`, pas sur l'identifiant de l'entité contenue dans la charge utile. Une même vente peut légitimement produire plusieurs livraisons — une par Pulse abonné, plus tout rejeu manuel que vous déclenchez vous-même.
</Tip>

Une livraison rejouée manuellement est une nouvelle livraison, avec son propre `x-pulse-delivery-id` : elle passera donc votre contrôle d'idempotence et sera traitée à nouveau. C'est le comportement voulu — le rejeu existe précisément pour retraiter un événement que votre point de terminaison a manqué.

## Diagnostiquer une signature qui ne correspond pas

| Symptôme                                                                                        | Cause probable                                                                                                                            |
| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Aucune représentation de l'empreinte ne correspond jamais                                       | Mauvais secret — une clé API ou un identifiant de boutique au lieu du secret `whsec_...` du Pulse                                         |
| Correspond parfois, échoue sur les charges utiles contenant des URL ou des caractères accentués | La charge utile a été re-sérialisée au lieu d'être hachée brute ; les barres obliques échappées ou les séquences `\uXXXX` ont été perdues |
| Toutes les signatures échouent depuis un changement récent                                      | Le secret a été renouvelé ; le point de terminaison utilise encore l'ancienne valeur                                                      |
| La comparaison lève une exception au lieu de renvoyer faux                                      | `timingSafeEqual` sur des tampons de longueurs différentes — le préfixe `sha256=` n'a été retiré que d'un seul côté                       |
| La signature est valide, mais le même événement est traité deux fois                            | Absence de déduplication sur `x-pulse-delivery-id`                                                                                        |

<Tip>
  Utilisez l'onglet **Livraisons** de la page du Pulse pour rejouer une livraison réelle pendant votre mise au point. Il affiche la charge utile exacte envoyée et la réponse exacte de votre point de terminaison, bien plus utile qu'un événement de test pour valider la vérification de bout en bout.
</Tip>

## Ressources associées

<CardGroup cols={2}>
  <Card title="Guide des Pulses" icon="bolt" href="/fr/guides/pulses">
    Événements, charges utiles, historique de livraison et réessais
  </Card>

  <Card title="Meilleures pratiques" icon="shield-check" href="/fr/guides/best-practices">
    Recommandations de sécurité et de fiabilité
  </Card>
</CardGroup>
