Le postback et le SDK envoient des données **vers** RefCampaign. Les webhooks font l'inverse : RefCampaign envoie un événement JSON signé **vers votre serveur** dès qu'un événement de cycle de vie d'une conversion se produit — pour synchroniser un CRM, déclencher une automatisation Slack ou Zapier, ou alimenter un pipeline de données sans polling.

Les webhooks sont disponibles à partir du plan **Growth**.

## Activer un webhook

Dans le dashboard, allez dans **Réglages → Webhooks → Ajouter un webhook** :

1. Saisissez l'URL de votre endpoint (obligatoirement en **HTTPS**).
2. Choisissez les événements à recevoir.
3. Enregistrez. Le secret de signature (`whsec_…`) est affiché **une seule fois** — conservez-le maintenant.

Vous pouvez configurer jusqu'à **5 webhooks** par compte, chacun avec son secret et ses abonnements. Utilisez **Envoyer un test** pour émettre un ping `webhook.test` ponctuel et vérifier que votre endpoint répond, et **Régénérer le secret** pour émettre un nouveau secret de signature.

## Événements

| Événement                     | Émis quand                                 |
| ----------------------------- | ------------------------------------------ |
| `conversion.created`          | Une conversion est enregistrée.            |
| `conversion.refunded`         | Une conversion est remboursée.             |
| `conversion.commission_paid`  | Une commission est marquée payée.          |
| `conversion.disputed`         | Le paiement d'une conversion est contesté. |
| `conversion.dispute_resolved` | Un litige de paiement est résolu.          |

## Payload

Chaque livraison est un `POST` avec un corps JSON de cette forme :

```json
{
  "event": "conversion.created",
  "data": { "conversionId": "conv_abc", "amount": 4990, "currency": "EUR" },
  "timestamp": "2026-06-19T12:00:00.000Z",
  "idempotencyKey": "conversion.created:conversion:conv_abc"
}
```

| Champ            | Type   | Notes                                                                                                                               |
| ---------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `event`          | string | L'un des types d'événements ci-dessus.                                                                                              |
| `data`           | object | Payload propre à l'événement.                                                                                                       |
| `timestamp`      | string | Heure d'émission ISO 8601.                                                                                                          |
| `idempotencyKey` | string | Stable par `(événement, ressource)`. Sert à dédupliquer — le même événement porte toujours la même clé, y compris lors des retries. |

## Vérifier la signature

Chaque requête porte un header `X-RefCampaign-Signature`, avec le même schéma que Stripe :

```
X-RefCampaign-Signature: t=<unix_seconds>,v1=<hex_sha256>
```

La signature est `HMAC-SHA256(secret, "<t>.<rawBody>")`. **Vérifiez-la toujours sur le corps brut de la requête, avant de parser le JSON** — un corps re-sérialisé ne correspondra pas.

<Callout type="warning" title="Vérifiez avant de faire confiance">
  Un webhook non vérifié peut être falsifié par quiconque connaît votre URL. Rejetez toute requête dont la signature n'est pas valide, et toute requête dont le timestamp sort de la fenêtre de tolérance (5 minutes par défaut) pour bloquer les rejeux.
</Callout>

Sur les stacks JS/TS, le SDK fournit un helper :

```ts
import { verifyWebhookSignature, WEBHOOK_SIGNATURE_HEADER } from '@refcampaign/sdk'

// Express — express.raw() pour garder le corps non parsé
app.post('/webhooks/refcampaign', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyWebhookSignature({
    secret: process.env.REFCAMPAIGN_WEBHOOK_SECRET,
    payload: req.body.toString('utf8'),
    header: req.header(WEBHOOK_SIGNATURE_HEADER) ?? '',
  })
  if (!ok) return res.status(400).send('signature invalide')

  const event = JSON.parse(req.body.toString('utf8'))
  switch (event.event) {
    case 'conversion.created':
      // …
      break
  }
  res.sendStatus(200)
})
```

Sur les autres stacks, recalculez le HMAC vous-même :

```python
import hmac, hashlib, time

def verify(secret: str, raw_body: str, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(','))
    ts, sig = int(parts['t']), parts['v1']
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f'{ts}.{raw_body}'.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
```

## Retries et idempotence

* Répondez avec un statut **2xx** dès que vous avez accepté l'événement. Tout statut non-2xx (ou un timeout au-delà de 10 s) est considéré comme un échec.
* Les livraisons en échec sont réessayées avec un backoff exponentiel (jusqu'à 5 tentatives).
* Les livraisons sont **idempotentes sur `idempotencyKey`** — un retry ne produit jamais de seconde livraison réussie, et votre handler doit traiter une clé répétée comme un no-op.
* Gardez votre handler rapide : faites le travail lourd en asynchrone et renvoyez 2xx immédiatement.

Chaque livraison (et son statut de réponse) est enregistrée dans **Réglages → Webhooks → Voir les livraisons** pour l'audit et le débogage.

## Sécurité

* Exposez votre endpoint uniquement en HTTPS.
* Stockez le secret de signature dans un gestionnaire de secrets — jamais dans le code source.
* Vérifiez la signature à chaque requête, sur le corps brut, avec une comparaison à temps constant.
* Régénérez le secret en cas de fuite — l'ancien secret cesse de valider immédiatement.
