Les webhooks sont la partie de la surface d'API d'un raccourcisseur d'URL que tout le monde propose, mais que presque personne ne propose bien. Les difficultés ne résident pas dans l'encodage — la charge utile est un objet JSON — mais dans les détails opérationnels : vérification de la signature, politique de réessai, idempotence, garanties de livraison, et ce qui se passe lorsque le point de terminaison de l'abonné est hors service pendant deux jours.
Cet article documente chaque type d'événement webhook qu'Elido émet, chaque format de charge utile, la courbe de réessai et le schéma de signature. Le guide de démarrage rapide API + SDK de raccourcisseur d'URL couvre la surface d'API entrante ; ceci est le côté sortant.
Les 12 types d'événements#
Elido émet 12 types d'événements webhook, regroupés en trois familles :
Événements de clic et de trafic : click, bio.click, qr.scan, conversion. Ceux-ci se déclenchent à chaque redirection ou scan après un court délai de file d'attente (décrit ci-dessous).
Événements de cycle de vie : link.created, link.updated, link.deleted, bio.published. Ceux-ci se déclenchent depuis la couche API lorsque l'enregistrement sous-jacent est modifié.
Événements d'agrégation et d'opérations : daily.summary, campaign.ended, alert.threshold_exceeded, quota.warning. Ceux-ci se déclenchent selon un calendrier ou lorsque le seuil est dépassé.
Un abonné enregistre un webhook via POST /v1/webhooks avec une URL cible et un tableau de types d'événements qu'il souhaite recevoir. La requête d'abonnement complète :
POST /v1/webhooks
Content-Type: application/json
Authorization: Bearer <api-key>
{
"url": "https://example.com/webhooks/elido",
"events": ["click", "conversion", "link.created"],
"secret": "whsec_<32-byte-base64>",
"active": true
}
Le secret est la clé HMAC utilisée pour signer les requêtes sortantes. Elle est opaque pour Elido ; nous ne l'enregistrons jamais et ne l'affichons jamais après la réponse à l'appel de création.
Le format de charge utile de l'événement click#
Par volume, c'est l'événement qui vous intéresse le plus. Chaque redirection via un lien court produit un événement click après que la redirection a été servie au client. Le format :
{
"id": "evt_2g8kFqJxYwPaZcvAm3HsTr",
"type": "click",
"created_at": "2026-05-22T14:32:18.847Z",
"data": {
"link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"short_url": "https://elido.me/abc123",
"destination_url": "https://shop.example.com/spring",
"click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
"ip_prefix": "203.0.113.0/24",
"country": "DE",
"city_geoname_id": 2950159,
"user_agent_family": "Chrome 124",
"device_type": "mobile",
"os_family": "iOS 17.5",
"referrer": "https://www.google.com",
"utm_source": "newsletter",
"utm_medium": "email",
"utm_campaign": "spring-2026",
"utm_term": null,
"utm_content": null
},
"workspace_id": "ws_12"
}
Quelques détails méritent d'être soulignés :
ip_prefixet nonip. Nous conservons le préfixe réseau /24 (IPv4) ou /48 (IPv6), et non l'adresse complète. L'article sur le RGPD pour les raccourcisseurs d'URL explique pourquoi ; l'effet pratique est que votre abonné obtient une précision géographique suffisante pour l'analyse sans la responsabilité liée aux données personnelles des IP complètes.city_geoname_idet noncity_name. L'identifiant GeoNames est stable quelle que soit la locale ; le nom de la ville varie. Si vous avez besoin d'un nom localisé, recherchez l'identifiant dans le dump de GeoNames.org une fois et mettez le résultat en cache.user_agent_family, et non la chaîne UA complète. Nous supprimons l'UA complète lors de l'ingestion (ce sont des données de fingerprinting à haute entropie) ; la famille est le navigateur + la version majeure qui subsiste.
Le délai entre la redirection servant le client et le déclenchement du webhook est généralement de 200ms à 2s. Les événements de clic transitent d'abord par Redpanda, sont agrégés pour l'analyse, puis un travailleur de diffusion émet les webhooks. C'est la même pipeline qui alimente l'analyse du tableau de bord — l'article sur l'ingestion de clics fire-and-forget couvre les mécanismes de file d'attente.
Le format de charge utile de l'événement conversion#
Les événements de conversion se déclenchent lorsqu'un clic est associé à une conversion en aval — un achat, une inscription, un formulaire de lead ou tout ce que vous connectez à la pipeline de transfert de conversion.
{
"id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
"type": "conversion",
"created_at": "2026-05-22T14:38:42.193Z",
"data": {
"click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
"link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"conversion_id": "cnv_2g8mPzKlYxRcBnQvFt5HwS",
"value": 49.50,
"currency": "EUR",
"event_name": "purchase",
"product_id": "sku_42",
"metadata": {
"order_id": "ord_12345",
"is_new_customer": true
},
"attribution_window_minutes": 6,
"forwarded_to": ["meta_capi", "ga4_mp"]
},
"workspace_id": "ws_12"
}
Le click_id renvoie à l'événement de clic d'origine ; vous pouvez joindre les deux côté serveur pour reconstruire le chemin du clic à la conversion. attribution_window_minutes est le temps écoulé entre le clic et le déclenchement de la conversion, ce qui est utile pour la modélisation de l'attribution.
Le tableau forwarded_to vous indique vers quelles plateformes de pixels Elido a déjà poussé cette conversion. Si votre abonné intègre des conversions dans votre propre entrepôt de données, vous pouvez utiliser cela pour éviter de compter deux fois dans vos analyses en aval.
Le format de charge utile de l'événement link.created#
Les événements de cycle de vie ont un format plus léger — juste la ressource et l'acteur :
{
"id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
"type": "link.created",
"created_at": "2026-05-22T14:38:42.193Z",
"data": {
"link": {
"id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"slug": "abc123",
"short_url": "https://elido.me/abc123",
"destination_url": "https://shop.example.com/spring",
"domain": "elido.me",
"tags": ["spring-2026", "newsletter"],
"created_at": "2026-05-22T14:38:42.193Z",
"created_by": "usr_42"
}
},
"workspace_id": "ws_12"
}
link.updated inclut un instantané previous aux côtés du nouvel état ; link.deleted inclut l'état final du lien au moment de la suppression. Le schéma complet se trouve dans le guide opérationnel /docs/guides/conversion-forwarding.
Vérification de la signature#
Chaque requête webhook inclut trois en-têtes HTTP :
Elido-Signature: t=1716392538,v1=4f3b2e1d9c8a7b6f5e4d3c2b1a0f9e8d7c6b5a49382716054 ...
Elido-Webhook-Id: evt_2g8kFqJxYwPaZcvAm3HsTr
Elido-Delivery-Attempt: 1
Le schéma de signature suit le modèle Stripe : HMAC-SHA256 sur {timestamp}.{body} en utilisant le secret du webhook. Le préfixe v1= est la version de l'algorithme de signature ; de nouvelles versions d'algorithmes sont ajoutées avant d'être définies par défaut, afin que les abonnés puissent vérifier plusieurs versions à la fois.
Vérification en Go :
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
)
func verify(sigHeader, body, secret string) bool {
parts := strings.Split(sigHeader, ",")
var t int64
var v1 string
for _, p := range parts {
kv := strings.SplitN(p, "=", 2)
if len(kv) != 2 { continue }
switch kv[0] {
case "t":
fmt.Sscanf(kv[1], "%d", &t)
case "v1":
v1 = kv[1]
}
}
if time.Since(time.Unix(t, 0)) > 5*time.Minute {
return false // rejeter les requêtes obsolètes
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.%s", t, body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(v1))
}
La vérification de la péremption de 5 minutes est l'élément que la plupart des abonnés oublient. Sans elle, une attaque par rejeu — un attaquant qui a capturé une requête valide et la rejoue plus tard — réussit car la signature est toujours valide. Avec la vérification de l'horodatage, la requête n'est acceptée que dans une fenêtre de 5 minutes après l'émission par Elido.
La spécification de signature est documentée dans la fiche pratique OWASP sur la sécurité des webhooks ; nous n'avons pas inventé le modèle, nous l'avons simplement implémenté.
Politique de réessai#
C'est la partie où la plupart des implémentations de webhooks deviennent négligentes.
Un webhook se déclenche une fois sur le chemin nominal : l'abonné renvoie 2xx, le répartiteur enregistre le succès, l'événement est terminé. Les cas plus difficiles sont les réponses non-2xx, les erreurs réseau et les abonnés qui répondent lentement.
Le calendrier de réessai d'Elido :
| Tentative | Délai après la précédente | Cumulé | Statut |
|---|---|---|---|
| 1 | — | 0 | initiale |
| 2 | 1s | 1s | premier réessai |
| 3 | 30s | 31s | |
| 4 | 5m | 5m 31s | |
| 5 | 1h | 1h 5m 31s | |
| 6 | 6h | 7h 5m 31s | |
| 7 | 24h | 31h 5m 31s | finale |
Après la tentative 7 (~31 heures après la première), le répartiteur abandonne et émet un événement interne webhook.failed. Le point de terminaison de l'abonné est marqué comme dégradé après trois échecs consécutifs sur n'importe quel événement ; les abonnements dégradés voient leur budget de réessai réduit pendant 24 heures. Après 50 échecs consécutifs, l'abonnement est suspendu et le propriétaire de l'espace de travail est notifié.
Le comportement de réessai respecte les en-têtes Retry-After de l'abonné. Si votre point de terminaison limite le taux d'Elido (renvoyant 429 avec Retry-After: 120), la tentative suivante attendra 120 secondes plutôt que les 30s par défaut du calendrier.
L'absence de réponse dans les 10 secondes est traitée comme un délai d'expiration et compte comme une tentative échouée. Le budget de 10 secondes est généreux à dessein — il couvre la latence de démarrage à froid des abonnés serverless — mais si votre point de terminaison prend régulièrement plus de 5 secondes, corrigez cela en priorité ; cela vous coûtera cher en volume de réessai.
Idempotence#
Les abonnés peuvent recevoir le même événement plusieurs fois.
Ce n'est pas un bug ; c'est la conséquence du fonctionnement de la distribution de messages distribués. Si un abonné renvoie une erreur 504 parce que son backend était lent mais a fini par traiter l'événement, le répartiteur retentera ; l'abonné le recevra deux fois et pourrait le traiter deux fois. Le même événement peut également se déclencher deux fois si le répartiteur plante pendant la livraison et que l'événement est remis dans la file d'attente.
La solution : chaque événement a un id unique (le préfixe evt_…). Les abonnés doivent stocker les identifiants qu'ils ont déjà traités (une petite table clé-valeur fonctionne ; un TTL de 14 jours couvre la fenêtre de réessai avec une marge) et ignorer les événements dont ils ont déjà vu l'identifiant.
CREATE TABLE webhook_processed_events (
event_id TEXT PRIMARY KEY,
received_at TIMESTAMPTZ DEFAULT now()
);
-- dans votre gestionnaire :
INSERT INTO webhook_processed_events (event_id) VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
-- si le RETURNING est vide, vous avez déjà traité cet événement
Le ON CONFLICT DO NOTHING est la vérification d'idempotence peu coûteuse. Si l'insertion renvoie une ligne, c'est la première fois que vous voyez l'événement ; si elle ne renvoie rien, vous l'avez déjà traité.
Pour les abonnés à haut débit (> 1k événements/sec), un SETNX Redis dédié avec TTL fonctionne de la même manière à un coût moindre qu'une ligne Postgres.
Ordre de livraison#
Il n'y a aucune garantie d'ordre global. Les événements provenant du même link_id sont répartis dans l'ordre de soumission, mais les événements provenant de liens différents peuvent arriver entremêlés. Un événement click à T+0 et un événement conversion à T+10ms pourraient arriver chez votre abonné dans n'importe quel ordre selon l'état du pool de travailleurs.
Les horodatages created_at font autorité pour le classement. Si votre abonné a besoin d'un ordre strict, triez par created_at côté serveur avant le traitement.
Pour le chemin clic → conversion spécifiquement : l'événement de conversion référence toujours le click_id de l'événement de clic, vous pouvez donc les joindre côté serveur même s'ils arrivent dans le désordre.
Webhooks vs polling — le compromis#
L'article sur les webhooks vs polling pour le suivi des clics couvre cela en détail. La réponse courte : les webhooks sont le bon modèle lorsque (a) vous avez besoin d'une faible latence à l'arrivée de l'événement (<5 secondes), et (b) votre abonné est accessible depuis l'internet public avec TLS. Le polling est le bon modèle lorsque (a) vous n'avez pas besoin du temps réel, (b) vous contrôlez l'entrepôt de données et voulez juste un lot quotidien/horaire, ou (c) votre abonné se trouve dans un réseau qui n'accepte pas le trafic entrant.
Pour la plupart des équipes, les webhooks sont la réponse. La courbe de réessai gère les échecs transitoires avec élégance ; le schéma de signature gère la sécurité ; le modèle d'idempotence gère la duplication de livraison. Le travail est du côté de l'abonné — construire un gestionnaire robuste — et ce travail est minime comparé à la construction d'une pipeline d'ingestion basée sur le polling.
Outils opérationnels#
La page webhook du tableau de bord affiche trois éléments par abonnement :
- Historique de livraison : chaque événement envoyé, le statut HTTP renvoyé par l'abonné, la latence et le prochain horodatage de réessai (le cas échéant).
- Rejeu : un bouton par événement pour le rejouer. Utile pour tester les modifications apportées à votre gestionnaire.
- Point de terminaison de test : un bouton par abonnement pour envoyer un événement de test synthétique sans déclencher un vrai clic. L'événement de test a
type: "test"et une charge utile fixe.
Le rejeu et les points de terminaison de test sont également exposés en tant qu'endpoints API (POST /v1/webhooks/{id}/events/{evt_id}/replay et POST /v1/webhooks/{id}/test).
Pour le débogage à haut débit, le guide d'observabilité couvre la façon de connecter la livraison des webhooks à vos propres métriques — chaque répartition est exportée sous forme de compteur Prometheus et d'histogramme.
Références externes#
- Fiche pratique de sécurité des webhooks OWASP — la justification du schéma de signature.
- Documentation des webhooks de Stripe — l'implémentation de référence pour les webhooks signés HMAC.
- RFC 7234 — HTTP/1.1 Caching — couvre la sémantique de
Retry-After.
Lectures connexes#
- Les smart links expliqués — la pierre angulaire du cluster de fonctionnalités.
- Webhooks vs polling pour le suivi des clics — quand choisir lequel.
- Guide de démarrage rapide API + SDK de raccourcisseur d'URL — la surface d'API entrante.
- Ingestion de clics fire-and-forget avec Redpanda — la file d'attente derrière le répartiteur.
- Suivi des conversions côté serveur — ce qui déclenche l'événement
conversion. - Surfaces produit :
/features/webhooks,/solutions/developers. - Guide opérationnel :
/docs/guides/conversion-forwarding,/docs/guides/observability.
Lire la suite
- FonctionnalitésRaccourcisseurs d'URL en marque blanche : ce que la marque blanche signifie vraiment
- FonctionnalitésAPI de réducteur d'URL : un démarrage rapide en 30 minutes dans cinq langues
- FonctionnalitésRaccourcisseur de liens avec un générateur de paramètres UTM — pourquoi le flux de travail s'intègre enfin