> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pulse-sell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Automatiser le traitement des ventes

> Recevoir sale.completed, vérifier la signature, dédoublonner et déclencher votre logistique ou votre CRM.

Objectif : dès qu'un paiement est confirmé, votre système agit (CRM, facture, message au client, préparation d'un colis) **une seule fois**, même si l'événement arrive en double.

## Le récepteur complet

Cet exemple Node.js réunit les trois règles d'or : vérifier la signature, répondre vite, ne traiter qu'une fois.

```javascript theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.PULSE_SECRET;
const seen = new Set(); // en production : une table avec contrainte d'unicité sur event.id

function verify(raw, header) {
  const p = Object.fromEntries((header || "").split(",").map((s) => s.trim().split("=")));
  if (!p.t || !p.v1 || Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const mac = crypto.createHmac("sha256", SECRET).update(`${p.t}.${raw}`).digest("hex");
  return mac.length === p.v1.length && crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(p.v1));
}

app.post("/webhooks/pulse-sell", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.get("x-pulsesell-signature"))) return res.sendStatus(400);

  const evt = JSON.parse(raw);
  res.sendStatus(200); // on répond tout de suite

  if (seen.has(evt.id)) return; // doublon : déjà traité
  seen.add(evt.id);
  handle(evt).catch((e) => console.error("traitement", evt.id, e));
});

async function handle(evt) {
  switch (evt.event) {
    case "sale.completed": {
      const { sale, customer } = evt.data;
      await crm.upsert({ email: customer.email, lastOrder: sale.number });
      await mailer.sendReceipt(customer.email, sale);
      break;
    }
    case "sale.abandoned":
      await whatsapp.remind(evt.data.customer, evt.data.sale.id);
      break;
    default:
      break; // événement inconnu ou futur : on l'ignore sans erreur
  }
}
```

## Aller plus loin

> **Relire la vente avant d'agir**
>
> L'événement contient déjà la vente, mais pour une action sensible (expédition, remise d'un accès), relisez-la avec `GET /sales/{id}` et vérifiez `payment_status === "completed"`. Vous êtes ainsi certain de l'état actuel, même si l'événement est arrivé en retard.

> **Marquer la vente livrée**
>
> Une fois votre livraison faite, appelez `POST /sales/{id}/fulfill` avec une `Idempotency-Key` dérivée de l'identifiant de la vente. Votre tableau de bord reflète alors la réalité.

> **Filtrer par produit**
>
> Créez un Pulse par famille de produits avec `product_ids` : une boutique qui vend des formations et des logiciels envoie chaque vente au bon système.

> **Rattraper une panne**
>
> Après une panne longue de votre serveur, listez les ventes manquées avec `GET /sales?from=…&to=…&per_page=100` et traitez-les avec la même logique dédoublonnée. Les Pulses relancent pendant environ 2 heures ; au-delà, c'est l'API qui comble le trou.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.