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

# Webhooks

> Recevoir les événements en temps réel, signés.

Un webhook prévient votre serveur en temps réel : DexNotify appelle une adresse HTTPS en `POST`, une fois par événement, avec une signature HMAC-SHA256.

Les webhooks se créent dans le dashboard, **Développeurs › Webhooks**, pour un projet et un environnement : l’URL de votre service et les événements voulus. Le secret (`whsec_…`) n’est montré qu’à la création et à chaque rotation. La création se fait par un membre, jamais par clé API.

## Événements

| Événement | Quand | Volume |
| - | - | - |
| `sms.sent` | un SMS accepté par l’opérateur | un appel par destinataire |
| `sms.delivered` | un accusé de réception | un appel par destinataire |
| `sms.failed` | un SMS refusé ou non remis, avec `errorCode` | un appel par destinataire |
| `sms.message.completed` | un envoi SMS conclu, avec ses compteurs et ses unités | une fois |
| `sms.balance.low` | le solde d’un projet passe sous son seuil | rare |
| `notification.sent` | un push accepté par Apple ou Google | un appel par appareil |
| `notification.failed` | un push refusé | un appel par appareil |
| `notification.opened` | une notification ouverte | — |
| `notification.clicked` | une notification, ou l’un de ses boutons, touchée | — |
| `campaign.completed` | une campagne push terminée, avec ses compteurs | une fois |
| `device.invalidated` | le fournisseur dit l’appareil disparu (jeton mort) | — |
| `device.unsubscribed` | l’app a désinscrit l’appareil | — |
| `inapp.clicked` | un élément d’un message in-app touché | — |
| `outcome.tracked` | une conversion, avec son attribution | — |
| `webhook.test` | envoyé à la demande, depuis la fiche du webhook | — |

> [!NOTE]
>
> Un envoi groupé à 150 000 destinataires produit 150 000 appels `sms.sent` puis autant de `sms.delivered` : dimensionnez votre point d’entrée en conséquence, ou ne vous abonnez qu’à `sms.message.completed`.

## La requête

```http theme={null}
POST https://votre-service/webhooks/dexnotify
Content-Type: application/json
User-Agent: DexNotify-Webhooks/1
DexNotify-Event: sms.delivered
DexNotify-Delivery: 6abe8f2c0ae0faf7c1f24a9e
DexNotify-Signature: t=1792915200,v1=5257a869e7…

{ "id": "1c0a…", "event": "sms.delivered", "environment": "production",
  "projectId": "6abe…", "occurredAt": "2026-10-26T09:14:00.000Z",
  "data": { "messageId": "6710…", "recipientId": "6711…", "sender": "DEXCHANGE",
            "phone": "+221771234567", "country": "SN", "segments": 1, "units": 1 } }
```

* **Dédoublonnez sur `id`** : il désigne le fait, et reste le même si la livraison est reprise.
* **Répondez `2xx` en moins de 10 s**, puis traitez.

## Vérifier la signature

`v1` est le HMAC-SHA256, en hexadécimal, de `<t>.<corps brut>` avec le secret du webhook. Calculez-le sur le corps **brut**, avant tout `JSON.parse`, et refusez une signature de plus de cinq minutes.

```ts Node.js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyDexNotifySignature(
  rawBody: string,
  header: string | undefined,
  secret: string,
  toleranceSeconds = 300,
): boolean {
  if (!header) return false;
  const parts = Object.fromEntries(
    header.split(",").map((part) => {
      const at = part.indexOf("=");
      return [part.slice(0, at).trim(), part.slice(at + 1).trim()];
    }),
  );
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}
```

```python Python theme={null}
import hashlib, hmac, time

def verify_dexnotify_signature(raw_body: bytes, header: str | None, secret: str, tolerance: int = 300) -> bool:
    if not header:
        return False
    parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    try:
        timestamp = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

```php PHP theme={null}
function verifyDexNotifySignature(string $rawBody, ?string $header, string $secret, int $tolerance = 300): bool
{
    if (!$header) return false;
    $parts = [];
    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        $parts[$key] = $value;
    }
    $timestamp = (int) ($parts['t'] ?? 0);
    if ($timestamp === 0 || abs(time() - $timestamp) > $tolerance) return false;
    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1'] ?? '');
}
```

Avec Express, gardez le corps brut :

```ts theme={null}
app.post("/webhooks/dexnotify", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyDexNotifySignature(raw, req.header("DexNotify-Signature"), process.env.DEXNOTIFY_WEBHOOK_SECRET!)) {
    return res.status(401).end();
  }
  res.status(204).end();
  const event = JSON.parse(raw);
  // Traitement après la réponse, dédoublonné sur event.id.
});
```

## Reprises et désactivation

| Réponse | Suite |
| - | - |
| `2xx` | livrée |
| erreur réseau, délai, `408`, `429`, `5xx` | reprise après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h ; `Retry-After` allonge le délai |
| autre `4xx` | échec définitif |
| `410 Gone` | échec définitif, et le webhook est désactivé |

Après 50 échecs définitifs d’affilée, le webhook est désactivé ; une livraison réussie remet le compteur à zéro. On le réactive depuis sa fiche, après un test. Le journal des livraisons est gardé 30 jours, et une livraison terminée se relance à la main.

## Sécurité

* **HTTPS obligatoire.**
* Une adresse qui résout vers un réseau privé ou local est refusée, et aucune redirection n’est suivie.
* La rotation du secret est immédiate : l’ancien cesse aussitôt.


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