4,7 % des soumissions du formulaire de démonstration produit d’un site SaaS headless créaient un second contact HubSpot strictement identique au premier, quelques secondes seulement après la soumission initiale. Ce taux, mesuré sur un mois de données, correspondait presque exactement au taux de doubles clics observé sur le bouton d’envoi lors des tests utilisateurs, combiné à quelques cas de nouvelles tentatives automatiques déclenchées par un intercepteur réseau du navigateur en cas de coupure momentanée.
Le problème ne se situait pas côté HubSpot, dont l’API se contente de créer ce qu’on lui demande de créer, mais dans l’absence de tout mécanisme empêchant deux requêtes identiques d’aboutir à deux créations distinctes côté endpoint WordPress relayant vers HubSpot.
D’où viennent les doubles soumissions
Trois scénarios expliquaient la quasi-totalité des doublons observés :
- Un double clic rapide sur le bouton d’envoi, avant que l’interface n’ait eu le temps de le désactiver visuellement
- Une nouvelle tentative automatique déclenchée par le navigateur lors d’une coupure réseau de quelques centaines de millisecondes
- Un retour arrière du navigateur suivi d’une nouvelle soumission du même formulaire déjà rempli
Première ligne de défense : upsert par e-mail côté HubSpot

L’API HubSpot propose une opération de type upsert basée sur l’adresse e-mail, qui met à jour un contact existant plutôt que d’en créer un nouveau si l’adresse est déjà connue :
const reponse = await fetch(
'https://api.hubapi.com/crm/v3/objects/contacts/' + encodeURIComponent(email) + '?idProperty=email',
{
method: 'PATCH',
headers: { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ properties: { email, source_demande: 'demo' } }),
}
);
Ce mécanisme couvre déjà une grande partie des cas de doublons liés à l’adresse e-mail. Il ne suffit cependant pas seul quand la logique métier associée à chaque soumission (envoi de notification interne, création d’une tâche de suivi commercial) ne doit, elle, s’exécuter qu’une seule fois par soumission réelle.
Deuxième ligne de défense : un identifiant d’idempotence
Un identifiant unique, généré côté client au moment où le formulaire s’affiche, est transmis avec la soumission et vérifié côté serveur avant tout traitement :
// Côté client, à l'affichage du formulaire
const idIdempotence = crypto.randomUUID();
// Côté endpoint WordPress
add_action('rest_api_init', function () {
register_rest_route('leads/v1', '/demo', [
'methods' => 'POST',
'callback' => 'traiter_demande_demo',
'permission_callback' => '__return_true',
]);
});
function traiter_demande_demo(WP_REST_Request $request) {
$id_idempotence = sanitize_text_field($request->get_param('id_idempotence'));
if (get_transient('demo_' . $id_idempotence)) {
return new WP_REST_Response(['statut' => 'deja_traite'], 200);
}
set_transient('demo_' . $id_idempotence, true, 15 * MINUTE_IN_SECONDS);
// traitement réel : envoi vers HubSpot, notification interne, etc.
return new WP_REST_Response(['statut' => 'ok'], 200);
}
Ce mécanisme couvre le cas du double clic et de la nouvelle tentative réseau automatique, puisque les deux requêtes portent le même identifiant d’idempotence généré une seule fois à l’affichage du formulaire, indépendamment de la valeur de l’e-mail saisi.
Désactiver visuellement le bouton dès le premier clic
Un correctif complémentaire, purement côté interface, désactive immédiatement le bouton d’envoi dès la première interaction, avant même que la requête réseau ne parte, réduisant mécaniquement la fenêtre de double clic à quasiment zéro :
const [envoiEnCours, setEnvoiEnCours] = useState(false);
async function soumettre(e) {
e.preventDefault();
if (envoiEnCours) return;
setEnvoiEnCours(true);
// ... envoi de la requête
}
Résultat après correction
Après déploiement des deux mécanismes combinés (upsert HubSpot et identifiant d’idempotence côté serveur), le taux de doublons mesuré est retombé à 0,1 % sur le mois suivant, ce résidu correspondant essentiellement à des cas où deux personnes différentes du même foyer partageaient temporairement une même adresse e-mail générique.
En résumé
La déduplication des leads transmis à HubSpot depuis un front headless se joue à trois niveaux complémentaires : l’upsert natif par e-mail côté HubSpot, un identifiant d’idempotence généré côté client et vérifié côté serveur, et une désactivation immédiate de l’interface au premier clic. Aucun de ces trois mécanismes pris isolément ne couvre l’ensemble des cas observés en production.