« Pourquoi passer par Zapier pour créer un contact HubSpot quand une seule requête HTTP suffit ? » C’est la question posée par un développeur en train de simplifier un tunnel de formulaires trop dépendant d’outils tiers payants. La réponse tient dans un bloc dynamique de formulaire qui appelle directement l’API CRM de HubSpot au moment de la soumission, sans webhook intermédiaire ni service d’automatisation externe.
Ce choix a un coût : le code du bloc doit gérer lui-même l’authentification, la structure de la requête et surtout les erreurs renvoyées par HubSpot (contact déjà existant, propriété invalide, quota dépassé). C’est plus de travail qu’un simple webhook Zapier, mais c’est aussi plus rapide à l’exécution et beaucoup moins coûteux à l’usage.
Créer une application privée HubSpot
Depuis 2022, HubSpot recommande les applications privées plutôt que les anciennes clés API globales, désormais dépréciées. Une application privée génère un jeton d’accès (pat-…) scoppé à des permissions précises — ici, la portée crm.objects.contacts.write suffit. Ce jeton est stocké côté WordPress dans une constante définie hors du répertoire public :
define( 'ACME_HUBSPOT_TOKEN', 'pat-eu1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' );
Le rendu du bloc formulaire
Le bloc est dynamique, avec un render_callback qui affiche un formulaire HTML classique (nom, e-mail, source de contact) posté vers un point de terminaison REST maison plutôt que directement vers HubSpot — cela évite d’exposer le jeton d’application côté navigateur.
add_action( 'rest_api_init', function() {
register_rest_route( 'acme/v1', '/lead', [
'methods' => 'POST',
'callback' => 'acme_create_hubspot_contact',
'permission_callback' => '__return_true',
] );
} );

L’appel à l’API CRM et la gestion des erreurs
La fonction de rappel construit la requête vers https://api.hubapi.com/crm/v3/objects/contacts avec wp_remote_post(), en interprétant systématiquement le code de statut retourné :
function acme_create_hubspot_contact( WP_REST_Request $request ) {
$email = sanitize_email( $request->get_param( 'email' ) );
if ( empty( $email ) || ! is_email( $email ) ) {
return new WP_Error( 'invalid_email', 'Adresse e-mail invalide.', [ 'status' => 400 ] );
}
$response = wp_remote_post( 'https://api.hubapi.com/crm/v3/objects/contacts', [
'headers' => [
'Authorization' => 'Bearer ' . ACME_HUBSPOT_TOKEN,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [
'properties' => [
'email' => $email,
'firstname' => sanitize_text_field( $request->get_param( 'firstname' ) ),
'lead_source' => 'bloc-formulaire-site',
],
] ),
'timeout' => 8,
] );
if ( is_wp_error( $response ) ) {
return new WP_Error( 'hubspot_unreachable', 'Service CRM injoignable.', [ 'status' => 502 ] );
}
$code = wp_remote_retrieve_response_code( $response );
if ( 409 === $code ) {
return rest_ensure_response( [ 'status' => 'already_exists' ] );
}
if ( $code >= 400 ) {
return new WP_Error( 'hubspot_rejected', 'Le CRM a refusé la création du contact.', [ 'status' => $code ] );
}
return rest_ensure_response( [ 'status' => 'created' ] );
}
Le code 409 mérite une attention particulière : HubSpot le renvoie quand un contact possède déjà cette adresse e-mail. Ce n’est pas une erreur au sens métier — le bloc doit alors afficher un message de confirmation plutôt qu’un message d’échec, sans quoi un visiteur récurrent recevrait toujours un message d’erreur trompeur.
Limites de quota et robustesse
Une application privée HubSpot sur un compte standard dispose d’un quota de 100 000 appels par jour, largement suffisant pour un formulaire de contact classique, mais un pic de trafic (campagne publicitaire, passage sur un plateau télé) peut ponctuellement s’en approcher. Le code doit prévoir le cas d’un code 429 (too many requests) et informer le visiteur que sa demande a été mise en attente plutôt que de la perdre silencieusement.
- Stocker chaque tentative en base locale avant l’appel API, pour pouvoir rejouer les échecs.
- Ne jamais bloquer l’interface du visiteur pendant l’appel réseau sans limite de temps (
timeoutexplicite). - Prévoir une file d’attente (table dédiée ou
wp_schedule_single_event()) pour les cas de code 429 ou 502.
Je recommande de toujours conserver une copie locale du lead avant l’appel HubSpot : en cas d’incident réseau côté CRM, la donnée n’est jamais perdue et peut être resynchronisée après coup.
Pour aller plus loin
Cette intégration directe convient à un flux de contact simple. Dès que la logique se complexifie — scoring multi-étapes, séquences d’e-mails conditionnelles, affectation automatique à un commercial — les workflows HubSpot natifs redeviennent pertinents, et il vaut mieux laisser le bloc se contenter de créer le contact, en confiant la suite à l’outil prévu pour cela.