Webhooks

Plutôt que d'interroger l'API en boucle, enregistrez une URL HTTPS et nous vous livrons les événements au fil de l'eau. Chaque livraison est signée, ce qui vous permet de prouver qu'elle vient bien de nous.

Enregistrer une URL

POST /api/v1/client/webhooks crée un abonnement et renvoie un secret.

curl -X POST "https://<hote-api-external>/api/v1/client/webhooks" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://votre-app.example.com/hooks/staffngo", … }'

Le secret est renvoyé une seule fois, à l'enregistrement. Il est stocké chiffré de notre côté et ne peut pas être relu. Récupérez-le immédiatement ; le perdre impose de réenregistrer.

Enregistrer deux fois la même URL ne crée pas de doublon — le même abonnement est renvoyé plutôt qu'un second. Votre endpoint doit être joignable publiquement en HTTPS : les adresses privées, de loopback ou non résolvables sont refusées d'emblée.

L'id renvoyé est une chaîne opaque : stockez-la et renvoyez-la telle quelle. Ne la lisez pas comme un nombre, n'en déduisez pas un autre, n'en fabriquez pas : les valeurs ne sont volontairement pas séquentielles, et une valeur que nous ne vous avons pas remise ne résoudra pas.

À quoi ressemble une livraison

En-tête Contenu
X-Webhook-Id Identifiant unique de l'événement — servez-vous-en pour dédoublonner.
X-Webhook-Event Type d'événement.
X-Webhook-Timestamp Temps Unix (secondes) au moment de la signature.
X-Webhook-Signature sha256=<HMAC en hexadécimal minuscule>

Vérifier la signature

La signature est un HMAC-SHA256, clé = votre secret, calculé sur la chaîne canonique {timestamp}.{corps brut} — le corps exactement tel que reçu, avant toute analyse JSON ou re-sérialisation.

import hmac, hashlib, time

def verify(secret: str, timestamp: str, raw_body: bytes, header: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        f"{timestamp}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()
    # Comparaison à temps constant — un == simple fuit la signature octet par octet.
    if not hmac.compare_digest(f"sha256={expected}", header):
        return False
    # Rejeter les horodatages trop anciens, sinon une livraison capturée reste rejouable.
    return abs(time.time() - int(timestamp)) < 300

Deux règles qui comptent plus qu'il n'y paraît :

  1. Comparez à temps constant. Une comparaison de chaînes naïve est un oracle temporel.
  2. Contrôlez l'horodatage. La signature seule n'expire pas ; sans fenêtre de fraîcheur, quiconque a capturé une livraison peut la rejouer indéfiniment.

Répondre

Répondez 2xx rapidement. Faites le travail réel de façon asynchrone — un handler lent grignote le délai de livraison et transforme un événement réussi en réessai.

Votre réponse Ce que nous faisons
2xx Livré. Terminé.
408, 429, 5xx Traité comme transitoire — réessayé avec backoff.
Tout autre 4xx Traité comme permanent — non réessayé, et des échecs répétés désactivent l'abonnement.
Timeout, DNS, TLS ou échec de connexion Transitoire — réessayé avec backoff.

Qui dit réessais dit handler idempotent : dédoublonnez sur X-Webhook-Id. Une livraison déjà traitée peut légitimement se représenter après un incident réseau de notre côté.

Tester avant la mise en service

POST /api/v1/client/webhooks/test envoie un ping à votre URL enregistrée : vous validez la joignabilité et votre vérification de signature sans attendre un événement réel. À utiliser à chaque modification du côté récepteur.