Webhooks
GetAff envoie un POST à votre point de terminaison HTTPS quand quelque chose se passe sur votre compte. Chaque livraison est signée ; votre code la vérifie avec un HMAC et n’a plus jamais à interroger.
Événements
Quatre événements, chacun un corps JSON { id, type, created_at, data }. data porte des identifiants et des comptes, jamais une adresse e-mail ni un message : lisez l’objet complet par l’API quand il vous le faut. id commence par msg_ et reste identique à chaque nouvel essai du même événement.
search.finished- Une recherche de partenaires d’un de vos sites s’est terminée — depuis l’écran, la conversation, l’API ou MCP.
data:siteId,searchId,results,onTopic,sources(par source :id,status,count),startedAt,finishedAt. prospect.replied- Un prospect a répondu à une séquence de prospection (détecté 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:applicationId,programId,programSlug,programName,affiliateName,affiliateSlug,score,autoApproved. partner.approved- Un partenariat sur l’un de vos programmes est devenu approuvé — votre décision, votre règle d’approbation automatique ou une invitation acceptée.
data:partnershipId,programId,programSlug,programName,affiliateName,origin.
Enregistrer un point de terminaison
Depuis Réglages du compte › API › Webhooks, ou par l’API : POST /v1/webhooks avec la portée write. La réponse porte le secret de signature (whsec_…) UNE SEULE FOIS ; conservez-le côté serveur. Jusqu’à dix points de terminaison par compte, chacun avec ses événements et son secret. L’URL doit être en https:// sur un hôte public : une adresse privée, de bouclage ou locale au lien, localhost ou un nom en .internal est refusé à l’enregistrement et de nouveau à chaque livraison.
curl -s https://www.getaff.org/api/v1/webhooks \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"url":"https://hooks.example.com/getaff",
"events":["prospect.replied","application.received"],
"description":"CRM sync"}'
# → { "data": { "endpoint": { "id": "whe_…", … }, "secret": "whsec_…" } }
# The secret is in THIS response only. Store it server-side.Ce qui arrive
Un POST avec trois en-têtes — webhook-id (l’identifiant du message), 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_) — et le corps JSON. Répondez un 2xx quelconque sous 10 secondes ; le corps de votre réponse est ignoré. Tout autre statut, un délai dépassé ou une erreur de connexion compte comme un essai échoué.
POST /getaff HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
webhook-id: msg_3f1c2b9a7d6e5f4c3b2a19087f6e5d4c
webhook-timestamp: 1762337524
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pfCnzRIJnS2bzE=
{"id":"msg_3f1c2b9a7d6e5f4c3b2a19087f6e5d4c","type":"prospect.replied",
"created_at":"2026-11-05T10:12:00.000Z",
"data":{"prospectId":"…","sequenceId":"…","mailboxEmail":"[email protected]",
"repliedAt":"2026-11-05T10:11:58.000Z"}}Vérifier une livraison
C’est le schéma Standard Webhooks : toute bibliothèque de ce nom vérifie une livraison GetAff avec le seul secret. Sans bibliothèque, quatre étapes :
- Lisez le corps BRUT de la requête tel que reçu — pas un objet resérialisé : une clé réordonnée change les octets et la signature ne correspond plus.
- Refusez un
webhook-timestampéloigné de plus de cinq minutes de votre horloge : c’est la fenêtre anti-rejeu. - Calculez le HMAC-SHA256 de
{webhook-id}.{webhook-timestamp}.{body}avec le secret décodé du base64, encodez-le en base64, et comparez en temps constant avec chaque valeurv1,…séparée par une espace danswebhook-signature. Une correspondance suffit (deux valeurs voyagent pendant une rotation de secret). - Traitez
idcomme une clé d’idempotence : un nouvel essai porte le même, et un récepteur qui mémorise les identifiants vus traite chaque événement une fois.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Express: app.post("/getaff", express.raw({ type: "application/json" }), handler)
// The body MUST be the raw bytes — never a parsed-then-stringified object.
export function verifyGetAffWebhook(headers, rawBody, secret, toleranceSec = 300) {
const id = headers['webhook-id'];
const ts = headers['webhook-timestamp'];
const sig = headers['webhook-signature'];
if (!id || !ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false; // replay window
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest();
return sig.split(' ').some((part) => {
const [version, value] = part.split(',');
if (version !== 'v1' || !value) return false;
const given = Buffer.from(value, 'base64');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
// Then: const event = JSON.parse(rawBody); dedupe on event.id; answer 200 at once.import { Webhook } from 'standardwebhooks';
const wh = new Webhook(process.env.GETAFF_WEBHOOK_SECRET.replace(/^whsec_/, ''));
const event = wh.verify(rawBody, headers); // throws when the signature or timestamp is wrongNouveaux essais et lettre morte
Un essai échoué est repris après 1 minute, puis 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 (Réglages du compte, ou GET /v1/webhooks/{id}), jamais reprise. Un hôte refusé ou un 404/410 de votre côté est définitif immédiatement. Les livraisons restent visibles trente jours. L’action « Tester » (ou POST /v1/webhooks/{id}/test) envoie tout de suite un événement webhook.test signé et montre la réponse de votre point de terminaison.
Faire tourner un secret
Réglages du compte › Rotation, ou POST /v1/webhooks/{id}/rotate. Pendant les 24 heures suivantes, chaque livraison porte deux signatures dans webhook-signature — nouvelle et ancienne, séparées par une espace — pour qu’un récepteur mis à jour plus tard dans la journée vérifie encore. Ensuite, l’ancien secret disparaît.
- Un secret est montré une fois et scellé au repos ; personne chez GetAff ne peut le relire. Perdu ? Faites une rotation.
- Les livraisons partent des serveurs de GetAff, jamais d’un navigateur, et ne portent jamais votre clé d’API.
- Un point de terminaison peut être mis en pause (
disabled) sans perdre son secret ; en pause, il ne reçoit rien et ses événements ne sont pas mis en file. - Vérifiez chaque livraison avant d’agir. Une requête non signée ou mal signée vers votre point de terminaison ne vient pas de GetAff.
- Répondez
2xxvite et travaillez ensuite : un traitement lent est un délai dépassé, et un délai dépassé est un nouvel essai que vous recevrez deux fois.