GetAff
Article

Connecter GetAff à votre CRM ou à Slack avec des webhooks signés

La réponse d’un créateur mérite d’être traitée dans l’heure, pas à la prochaine revue hebdomadaire. Les webhooks permettent à GetAff de prévenir vos systèmes dès qu’il se passe quelque chose — une recherche terminée, un prospect qui répond, un créateur qui candidate, un partenariat approuvé. Votre CRM reçoit la mise à jour et votre équipe le message Slack, sans que personne n’ait à interroger quoi que ce soit.

Cet article présente les quatre événements, l’enregistrement d’un point de terminaison, la vérification de la signature en Node et en Python, le calendrier des nouveaux essais et deux intégrations concrètes.

Mis à jour le

Les quatre événements

Chaque livraison est un corps JSON { id, type, created_at, data }. data porte des identifiants et des comptes, jamais une adresse e-mail ni un message : quand il vous faut l’objet complet, lisez-le par l’API à partir de l’identifiant.

  • search.finished — une recherche de partenaires sur l’un de vos sites s’est terminée, qu’elle ait été lancée depuis l’écran, la conversation, l’API ou MCP. data : siteId, searchId, results, onTopic, sources par source, startedAt, finishedAt.
  • prospect.replied — un prospect a répondu à une séquence de prospection, réponse détectée dans votre boîte par le tick de dix minutes. data : prospectId, sequenceId, mailboxEmail, repliedAt. Un stade déplacé à la main ne le déclenche pas.
  • application.received — un créateur a candidaté à l’un de vos programmes. data contient notamment applicationId, programSlug, affiliateName, score et autoApproved.
  • partner.approved — un partenariat est devenu approuvé : par votre décision, par votre règle d’approbation automatique ou par une invitation acceptée. data contient notamment partnershipId, programSlug, affiliateName et origin.

Enregistrer un point de terminaison

Depuis Réglages du compte › API › Webhooks, ou avec une clé dotée de la portée write :

curl -s -X POST https://www.getaff.org/api/v1/webhooks \
  -H "Authorization: Bearer $GETAFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.example.com/getaff",
       "events": ["prospect.replied", "application.received"],
       "description": "CRM sync"}'

La réponse contient le secret de signature (whsec_…) une seule fois : conservez-le côté serveur, avec vos autres secrets. Un compte peut avoir jusqu’à dix points de terminaison, chacun avec ses propres événements et son propre secret. L’URL doit être en https:// sur un hôte public ; les adresses privées, de bouclage et locales au lien sont refusées à l’enregistrement, puis de nouveau à chaque livraison.

Vérifier chaque livraison

Les livraisons suivent la spécification Standard Webhooks. Trois en-têtes accompagnent le corps : webhook-id, webhook-timestamp (secondes Unix) et webhook-signature — v1, suivi du HMAC-SHA256 en base64 de {id}.{timestamp}.{body}, avec pour clé les octets derrière whsec_. Toute bibliothèque Standard Webhooks la vérifie avec le seul secret ; sans bibliothèque, une douzaine de lignes suffisent.

  1. Lisez le corps BRUT tel que reçu. Un objet resérialisé change les octets, et la signature ne correspond plus.
  2. Refusez un horodatage éloigné de plus de cinq minutes de votre horloge : c’est la fenêtre anti-rejeu.
  3. Calculez le HMAC et comparez-le en temps constant avec chaque valeur v1,… séparée par une espace. Une correspondance suffit — deux valeurs voyagent pendant une rotation de secret.
  4. Mémorisez le webhook-id traité : un nouvel essai porte le même.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, headers, secret) {
  const id = headers['webhook-id'];
  const ts = Number(headers['webhook-timestamp']);
  const sigs = (headers['webhook-signature'] ?? '').split(' ');
  if (!id || !ts || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const expected = Buffer.from(
    'v1,' + createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64'),
  );
  return sigs.some((s) => {
    const got = Buffer.from(s);
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}
Python
import base64, hashlib, hmac, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("webhook-id", "")
    ts = headers.get("webhook-timestamp", "")
    if not msg_id or not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{ts}.".encode() + raw_body
    expected = "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(s, expected)
               for s in headers.get("webhook-signature", "").split(" "))

Une requête non signée ou mal signée vers votre point de terminaison ne vient pas de GetAff. Les livraisons ne portent jamais votre clé d’API.

Répondre vite, prévoir les nouveaux essais

Répondez un 2xx quelconque sous dix secondes ; le corps de votre réponse est ignoré. Tout le reste — un autre statut, un délai dépassé, une erreur de connexion — compte comme un essai échoué, repris après 1 minute, 5 minutes, 30 minutes, 2 heures et 12 heures : six essais sur environ quinze heures, toujours avec le même webhook-id et le même corps. Après le sixième, la livraison passe en lettre morte : conservée et visible dans Réglages du compte ou via GET /v1/webhooks/{id}, jamais reprise. Un 404 ou un 410 de votre côté est définitif immédiatement.

  • Accusez réception d’abord, travaillez ensuite : placez l’événement dans une file et renvoyez 200.
  • Dédoublonnez sur webhook-id, pour qu’un nouvel essai consécutif à une réponse lente ne soit traité qu’une fois.
  • Utilisez l’action « Tester », ou POST /v1/webhooks/{id}/test, pour envoyer un événement webhook.test signé et voir la réponse de votre point de terminaison.

Exemple : synchroniser votre CRM

Abonnez un point de terminaison à prospect.replied et partner.approved. Sur une réponse, récupérez le prospect avec GET /v1/prospects/{prospectId} (statut, canal de contact, provenance), puis créez ou mettez à jour le contact dans votre CRM et ouvrez une tâche pour le responsable du compte. Sur une approbation, passez l’affaire au stade « partenaire » et enregistrez le partnershipId, pour que la finance puisse ensuite y rapprocher les commissions.

Comme data contient des identifiants et non des données personnelles, l’événement lui-même peut être journalisé sans risque ; le détail ne transite que par votre appel authentifié à l’API.

Exemple : un message Slack pour chaque candidature

Les webhooks entrants de Slack acceptent un simple POST en JSON. Après avoir vérifié la signature, transmettez un court résumé de application.received :

Transmettre à Slack
if (event.type === 'application.received') {
  const d = event.data;
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `New application to ${d.programName} from ${d.affiliateName} (score ${d.score})`,
    }),
  });
}

search.finished se prête aussi bien à un récapitulatif : publiez le nombre de résultats dans le sujet et un lien vers la page « Trouver » du site, pour que l’équipe examine une nouvelle recherche le jour même.

Un point de terminaison par usage

Chaque point de terminaison a ses propres événements et son propre secret : séparez-les par usage plutôt que de tout envoyer à une seule URL. Une synchronisation CRM qui n’a besoin que de prospect.replied et partner.approved n’a pas à recevoir search.finished ; un relais Slack pour l’équipe partenariats peut se contenter de application.received. Des gestionnaires plus petits sont plus simples à vérifier, à surveiller et à mettre en pause.

Chaque point de terminaison affiche aussi le statut de sa dernière livraison et ses vingt dernières tentatives, avec la réponse de votre serveur. Quand un récepteur casse, cet historique dit si le problème vient de votre code, de votre certificat ou de votre pare-feu — et la lettre morte garde trente jours chaque événement manqué : rien n’est perdu pendant la correction.

Faire tourner un secret sans interruption

Lancez la rotation depuis Réglages du compte ou avec POST /v1/webhooks/{id}/rotate. Pendant les 24 heures suivantes, chaque livraison porte deux signatures — la nouvelle et l’ancienne —, si bien qu’un récepteur mis à jour plus tard dans la journée vérifie encore. Un point de terminaison peut aussi être mis en pause sans perdre son secret ; en pause, il ne reçoit rien et rien n’est mis en file. La documentation des webhooks détaille le contrat complet, et la référence chaque point de terminaison.

Documentation

Le contrat exact, généré à partir des schémas que les routes appliquent :

À lire ensuite