12 min de lectureIntégrations

Webhooks vs polling pour le suivi des clics - choisir le bon motif

Une analyse pratique du moment où utiliser les webhooks et du moment où interroger l'API d'analyses pour les données de clic : coûts cachés de chaque approche, exemples de code concrets en TypeScript et Python, et le motif hybride qui couvre la plupart des cas d'usage en production.

Sasha Ehrlich
Compliance · EU residency
Split diagram comparing webhooks and polling for click tracking: left panel shows server pushing click.recorded events to receiver endpoint; right panel shows cron hitting /analytics/summary every N minutes

Deux équipes construisant sur la même API de raccourcisseur d'URL finissent souvent avec des architectures d'intégration complètement différentes. Une équipe configure un point de terminaison webhook et réagit à chaque clic en temps réel. L'autre écrit un cron job qui interroge l'API d'analyses toutes les cinq minutes. Les deux sont valides. La décision entre eux a de réelles conséquences pour la latence, le surcoût opérationnel et le degré de dégradation de votre système lorsqu'un problème survient.

Cet article expose les véritables compromis.

Les deux motifs#

Polling#

Le polling signifie que votre code demande à l'API les données récentes de clics selon un calendrier. Un cron job se réveille, appelle /v1/analytics/workspaces/{id}/clicks/recent ou /v1/analytics/workspaces/{id}/summary, traite les résultats, puis dort jusqu'à l'intervalle suivant.

Le flux de données est basé sur la traction : votre infrastructure initie chaque interaction. Le serveur API n'a aucune connaissance de vos systèmes internes - il répond simplement aux requêtes que vous envoyez.

Webhooks#

Les webhooks signifient que le serveur d'Elido pousse un événement click.recorded vers votre point de terminaison HTTPS peu après le traitement d'un clic. Votre récepteur le gère, retourne un 2xx, et la livraison est enregistrée comme réussie.

Le flux de données est basé sur la poussée : la plateforme initie le contact. Votre point de terminaison doit être accessible depuis Internet, doit avoir TLS et doit répondre de manière fiable.

Diagramme côte à côte : le polling montre votre cron job envoyant GET à l'API Elido et récupérant des lignes toutes les cinq minutes ; les webhooks montrent le serveur Elido poussant un POST click.recorded vers votre point de terminaison HTTPS qui retourne 2xx.

Quand le polling est le bon choix#

Le polling convient à un ensemble spécifique de conditions. Si la plupart de celles-ci s'appliquent à votre situation, commencez par le polling et n'envisagez les webhooks que lorsqu'un problème concret vous y oblige.

Vous contrôlez les deux côtés de l'intégration. Lorsque le consommateur est un tableau de bord ou un outil de reporting que vous possédez et exploitez, le polling vous donne un comportement prévisible et borné. Vous décidez de l'intervalle ; vous décidez de la fenêtre temporelle ; vous décidez comment gérer les résultats partiels.

Votre cas d'usage est rétrospectif. Les rapports de campagne hebdomadaires, les tâches d'agrégation mensuelles et les pipelines de réconciliation ne bénéficient pas d'une latence inférieure à la minute. Un cron job s'exécutant toutes les heures contre /summary ou /breakdown/country est architecturalement plus simple et plus facile à raisonner qu'un récepteur webhook avec état et gestion de réessais.

Vous n'avez aucun point de terminaison public à exposer. Les webhooks nécessitent une URL accessible depuis l'infrastructure d'Elido. Si votre intégration s'exécute à l'intérieur d'un réseau privé, une fonction Lambda sans URL stable ou la machine locale d'un développeur, configurer un point de terminaison HTTPS entrant peut coûter plus en complexité opérationnelle que le bénéfice de latence n'en vaut la peine.

Le volume est faible. À quelques milliers de clics par jour, la différence entre temps réel et un décalage de cinq minutes est rarement visible pour les utilisateurs finaux. Le polling est simple à comprendre, simple à déboguer et ne produit aucune surprise d'infrastructure.

Quand les webhooks sont le bon choix#

Les webhooks ont du sens lorsque la latence est une exigence produit plutôt qu'un plus.

Vous construisez un compteur en direct ou une UX en temps réel. Si votre produit montre aux utilisateurs un nombre de clics qui se met à jour visiblement en quelques secondes après une redirection, le polling à tout intervalle raisonnable semblera notablement obsolète. Un gestionnaire webhook qui incrémente un compteur Redis sur les événements click.recorded et le fait remonter via une connexion WebSocket ou SSE au frontend est l'architecture qui atteint cela sans matraquer l'API d'analyses.

Vous enrichissez des enregistrements CRM par clic. Lier un événement de clic à un enregistrement de contact - identifier quel prospect spécifique a suivi le lien dans votre email sortant et mettre à jour sa chronologie CRM - est sensible au temps. Au moment où une tâche de polling rattrape cinq minutes plus tard, le commercial peut avoir déjà appelé. Un gestionnaire webhook qui déclenche une mise à jour CRM dans les secondes suivant le clic est le bon outil.

Vous exécutez des workflows pilotés par événements. Les workflows déclenchés par des événements de clic - envoi d'un email de suivi lorsqu'un lien est cliqué, mise à jour du segment d'un abonné, décrémentation d'un compteur d'inventaire - sont des consommateurs webhooks naturels. L'événement click.recorded porte suffisamment de données pour agir immédiatement, sans aller-retour de requête.

Vous avez un point de terminaison HTTPS stable et publiquement accessible. C'est le prérequis dont tout le reste dépend. Si vous avez déjà une infrastructure de production qui accepte des webhooks entrants d'autres fournisseurs (Stripe, GitHub, Twilio), ajouter Elido au même récepteur est à faible friction.

Les coûts cachés des webhooks#

Les webhooks semblent simples : le serveur envoie un POST, vous le gérez. La véritable surface d'implémentation est plus large.

Diagramme en couches des quatre portes que franchit chaque livraison click.recorded entrante : vérification de signature HMAC, fenêtre de rejeu de 300 secondes, déduplication d'idempotence sur l'ID de livraison, et disponibilité avant de retourner 2xx.

Vérification de signature#

Elido signe chaque livraison de webhook avec HMAC-SHA256. Le format de signature est v1=HMAC-SHA256(secret, "${unix_timestamp}.${body}"), livré dans l'en-tête X-Webhook-Signature. L'horodatage est envoyé séparément dans X-Webhook-Timestamp. Les deux sont produits dans services/webhook-dispatcher/internal/signing/hmac.go.

Vous devez vérifier cette signature avant de traiter la charge utile. Un récepteur qui saute la vérification traitera tout POST qui atteint le point de terminaison, y compris les requêtes usurpées par quiconque découvre votre URL webhook.

Voici un gestionnaire Express minimal en TypeScript qui vérifie la signature avant de faire quoi que ce soit avec la charge utile :

import express, { Request, Response } from "express";
import crypto from "crypto";

const app = express();

// Use raw body middleware - JSON parsers consume the stream before you can hash it
app.use("/webhook", express.raw({ type: "application/json" }));

function verifySignature(
  secret: string,
  signature: string,
  timestamp: string,
  rawBody: Buffer,
): boolean {
  const message = `${timestamp}.${rawBody.toString("utf8")}`;
  const expected =
    "v1=" + crypto.createHmac("sha256", secret).update(message).digest("hex");
  // Use timingSafeEqual to prevent timing-based enumeration
  return crypto.timingSafeEqual(
    Buffer.from(signature, "utf8"),
    Buffer.from(expected, "utf8"),
  );
}

app.post("/webhook", (req: Request, res: Response) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const timestamp = req.headers["x-webhook-timestamp"] as string;

  if (!signature || !timestamp) {
    return res.status(400).json({ error: "missing signature headers" });
  }

  // Reject payloads older than 5 minutes
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (age > 300) {
    return res.status(400).json({ error: "payload too old" });
  }

  if (
    !verifySignature(
      process.env.WEBHOOK_SECRET!,
      signature,
      timestamp,
      req.body as Buffer,
    )
  ) {
    return res.status(401).json({ error: "invalid signature" });
  }

  const event = JSON.parse((req.body as Buffer).toString("utf8"));

  if (event.type === "click.recorded") {
    // Handle the click event
    console.log("click recorded:", event.data);
  }

  // Always return 2xx promptly - do heavy processing async
  return res.status(200).json({ received: true });
});

La fenêtre de rejeu#

La vérification d'horodatage dans l'exemple ci-dessus impose ce que la documentation d'Elido appelle une fenêtre de rejeu. Sans elle, un attaquant qui capture une seule charge utile signée valide peut la rejouer indéfiniment - la signature reste valide pour toujours car elle est calculée à partir d'un horodatage fixe. Avec la vérification, une charge utile plus ancienne que cinq minutes est rejetée quelle que soit la validité de la signature.

Réglez la tolérance à quelque chose que votre infrastructure peut gérer. Cinq minutes est la valeur par défaut conventionnelle et correspond à ce qu'utilise Stripe. Si votre récepteur passe occasionnellement hors ligne pendant quelques minutes lors des déploiements, cette fenêtre lui donne le temps de redémarrer et de traiter quand même les livraisons en cours.

Réessais et idempotence#

Le webhook-dispatcher d'Elido réessaie les livraisons échouées sur un calendrier de backoff : premier réessai à 1 minute, deuxième à 5 minutes, troisième à 15 minutes. Le nombre maximum de tentatives par livraison est de 3 par défaut, comme défini dans le schéma webhook_deliveries. Après 3 tentatives échouées, la livraison est marquée comme définitivement échouée et apparaît dans le tableau de bord des notifications.

Cela signifie que votre récepteur peut recevoir le même événement plus d'une fois. Tout traitement ayant des effets de bord - écriture dans une base de données, envoi d'un email, mise à jour d'un compteur - doit être idempotent. L'en-tête X-Webhook-Delivery porte un ID de livraison stable que vous pouvez utiliser comme clé d'idempotence.

// Before processing, check whether this delivery has already been handled
const deliveryId = req.headers["x-webhook-delivery"] as string;
const alreadyProcessed = await redis.get(`webhook:delivery:${deliveryId}`);
if (alreadyProcessed) {
  return res.status(200).json({ received: true, duplicate: true });
}
// Mark as processed with a TTL that covers the retry window
await redis.set(`webhook:delivery:${deliveryId}`, "1", "EX", 3600);

Votre point de terminaison doit être hautement disponible#

La fenêtre de réessai est finie. Si votre récepteur est hors service pendant plus d'environ 21 minutes (1 + 5 + 15), les livraisons épuiseront leurs tentatives et échoueront définitivement. Pour les événements où la livraison garantie compte - enrichissement CRM, hooks de facturation - votre infrastructure de récepteur a besoin d'une disponibilité appropriée, pas d'un serveur amateur qui redémarre occasionnellement.

C'est le coût le plus sous-estimé des webhooks pour les équipes nouvelles dans le HTTP entrant. Le polling se dégrade gracieusement : si la tâche de polling échoue, elle s'exécute simplement à nouveau à l'intervalle suivant et rattrape. Un récepteur webhook indisponible perd les événements définitivement à moins que vous n'ayez une stratégie de réconciliation.

Les coûts cachés du polling#

Le polling semble simple de l'extérieur. Les coûts réels s'accumulent en production.

Le décalage est la contrainte définissante. Un cron job s'exécutant toutes les cinq minutes signifie que les données de clic sont obsolètes jusqu'à cinq minutes. Pour la plupart des cas d'usage rétrospectifs c'est acceptable ; pour quoi que ce soit orienté utilisateur, ça ne l'est pas. Raccourcir l'intervalle aide mais n'élimine pas le décalage, et des intervalles très courts (moins d'une minute) commencent à ressembler à du matraquage API plutôt qu'à du polling.

Requêtes gaspillées. La plupart des intervalles de polling retournent les mêmes données que la requête précédente. Si vous interrogez un lien à faible trafic toutes les minutes et que les clics arrivent à environ un par heure, 59 requêtes sur 60 ne retournent rien de nouveau. Ces requêtes comptent toujours contre votre limite de débit API.

Limites de débit. L'API d'Elido applique des limites de débit par espace de travail dimensionnées par palier de facturation. Une tâche de polling qui s'exécute fréquemment à travers de nombreux liens dans un grand espace de travail peut atteindre ces limites, en particulier si d'autres automatisations dans le même espace de travail effectuent également des appels API. L'API retourne 429 Too Many Requests avec un en-tête X-RateLimit-Scope: workspace lorsque cela se produit.

Pagination et événements manqués. Le point de terminaison /clicks/recent utilise une pagination basée sur un curseur. Si vous interrogez sur une fenêtre temporelle fixe - ?from=<last_poll>&to=<now> - et que le volume dans cette fenêtre dépasse la taille de page, vous manquerez des événements à moins de suivre next_cursor à travers toutes les pages. Une implémentation de polling qui ne gère pas la pagination perdra silencieusement des données sous la charge.

Le motif hybride#

Pour la plupart des cas d'usage en production, la meilleure réponse n'est pas l'un ou l'autre.

Diagramme d'architecture hybride : Elido pousse les événements de clic vers un gestionnaire webhook comme chemin temps réel principal, tandis qu'un polling nocturne de /summary et /timeseries réconcilie les totaux dans votre stockage et comble les écarts laissés par la fenêtre de réessai.

Utilisez les webhooks comme chemin principal pour la réaction en temps réel : mises à jour CRM, compteurs en direct, workflows pilotés par événements. La latence est faible ; le surcoût opérationnel est gérable si vous avez déjà une infrastructure HTTPS entrante.

Utilisez le polling comme passe de réconciliation hebdomadaire ou quotidienne : tirez la série temporelle complète pour la semaine précédente, comparez les totaux à ce que votre gestionnaire webhook a enregistré et identifiez les écarts. Cela attrape les livraisons qui ont épuisé leur fenêtre de réessai pendant une panne, les événements arrivés hors ordre et toute divergence entre votre état local et la source de vérité d'Elido.

L'API d'analyses est bien adaptée à ce rôle. Le point de terminaison /summary retourne les totaux agrégés pour une plage de dates en une seule requête ; le point de terminaison /timeseries retourne des buckets quotidiens. Une tâche de réconciliation qui s'exécute une fois par nuit et compare les comptes de clics enregistrés par votre CRM au résumé de l'API pour la même fenêtre peut faire apparaître les problèmes d'intégrité des données avant qu'ils ne deviennent visibles pour les clients.

Un cron de polling en Python#

Pour les équipes qui veulent commencer par le polling et passer aux webhooks plus tard, voici une implémentation minimale utilisant la bibliothèque schedule qui appelle /clicks/recent à un intervalle de cinq minutes :

import schedule
import time
import requests
import os

API_BASE = "https://api.elido.app/v1/analytics"
WORKSPACE_ID = os.environ["ELIDO_WORKSPACE_ID"]
API_KEY = os.environ["ELIDO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# Track the cursor across poll intervals so we only fetch new clicks
_cursor = None

def poll_recent_clicks():
    global _cursor
    params = {"limit": 100}
    if _cursor:
        params["cursor"] = _cursor

    while True:
        resp = requests.get(
            f"{API_BASE}/workspaces/{WORKSPACE_ID}/clicks/recent",
            headers=HEADERS,
            params=params,
            timeout=10,
        )
        resp.raise_for_status()
        body = resp.json()

        items = body.get("items", [])
        for click in items:
            process_click(click)

        next_cursor = body.get("next_cursor")
        if not next_cursor:
            # Persist the current cursor for the next run
            if items:
                _cursor = None  # reset: next poll fetches from now
            break
        params["cursor"] = next_cursor

def process_click(click: dict):
    # Replace with your actual processing logic
    print(f"click: link={click['link_id']} country={click.get('country_code')}")

schedule.every(5).minutes.do(poll_recent_clicks)

if __name__ == "__main__":
    poll_recent_clicks()  # run once on startup to catch up
    while True:
        schedule.run_pending()
        time.sleep(10)

Dans un déploiement en production, remplacez le print par votre puits réel - une écriture en base de données, un appel API CRM, une publication dans une file de messages - et ajoutez une gestion d'erreur avec un backoff exponentiel autour de l'appel requests.get.

Filtrage des bots et ce que cela signifie pour votre intégration#

Un détail qui affecte les deux motifs : le service edge-redirect d'Elido filtre les clics de bots avant d'émettre des événements de clic vers le pipeline de traitement. Les requêtes provenant de Googlebot, Bingbot, Slackbot, des moniteurs de disponibilité, curl, des bibliothèques de scripting et des User-Agents vides ne produisent pas d'événements click.recorded et n'apparaissent pas dans les résultats de l'API d'analyses.

Cela compte parce que cela signifie que votre gestionnaire webhook ou tâche de polling travaille avec des comptes de redirection humaine, pas avec des comptes de requêtes HTTP brutes. Si vous corrélez les données de clic Elido aux métriques côté serveur - les logs serveur de votre application, les logs d'accès d'un CDN - attendez-vous à ce que les chiffres d'Elido soient inférieurs. La divergence n'est pas un bug ; c'est le filtre de bots qui supprime le bruit avant qu'il ne vous atteigne.

Pour plus de détails sur ce que couvre le filtre de bots et comment le scoreur de suspicion marque le trafic limite, le guide d'analyses contient une ventilation complète. Pour les propriétés de sécurité du schéma de signature webhook - y compris le format HMAC, le lien d'horodatage et ce qu'il empêche - consultez la checklist de sécurité.


La page de tarification contient la ventilation des paliers de forfait qui incluent les points de terminaison webhook et à quels plafonds de volume de livraison.

Articles liés sur le blog#

Essayer Elido

Collez une URL, obtenez un lien court

Sans inscription. Lien actif 30 jours. Inscrivez-vous pour le garder pour toujours.

Gratuit, sans inscription · 2 par jour

Essayer Elido

Raccourcisseur d'URL hébergé en UE : domaines personnalisés, analyses approfondies et API ouverte. Forfait gratuit - sans carte bancaire.

Tags
click tracking
webhooks
url shortener api
link analytics
api integration
event-driven
polling
real-time analytics

Lire la suite