Media buying · la plomberie

Brancher Meta soi‑même

Se connecter au compte par l'API, poser ses pixels, envoyer ses conversions serveur, publier ses annonces, et surtout vérifier que la chaîne dit la vérité. Tout ce qui se passe entre le clic et le chiffre affiché dans le rapport.

Écrit après avoir reconstruit ce système de bout en bout, agence sortie du circuit.
Chaque piège listé ici a coûté quelque chose.

01Le vocabulaire, une fois pour toutes

Les mots changent selon l'écran où on se trouve, et deux personnes croient parler de la même chose alors que non. À poser avant tout le reste.

Ce qu'on ditCe que c'estOù ça vit
Business Manager, Portfoliole coffre qui contient tout le restebusiness.facebook.com
Appl'identité technique qui parle à l'APIdevelopers.facebook.com/apps
System Userun « employé robot » porteur du jetonParamètres du business
Pixel, Datasetle collecteur d'évènementsGestionnaire d'évènements
Compte publicitairelà où l'argent est débitéact_<id>
Évènementune action nommée (Lead, Purchase, ou un nom à soi)dans le pixel
CAPIl'envoi d'évènements depuis ton serveurton code

02Se connecter à Meta par l'API

C'est l'étape que tout le monde rate, parce que l'interface propose partout des jetons qui périment en une heure ou en soixante jours. Un système qui tourne en cron a besoin d'un jeton permanent, et il n'en existe qu'un type : celui d'un System User.

La chaîne complète, dans l'ordre

Business Manager
   ├── App  (developers.facebook.com/apps)   → app_id + app_secret
   └── System User  (Paramètres du business → Utilisateurs système)
         ├── on lui AFFECTE les actifs : compte pub, pixel, page
         └── on génère SON jeton, en cochant les permissions
               → ce jeton-là ne périme pas

Les permissions à cocher, ni plus ni moins :

PermissionPour quoi
ads_managementcréer et modifier campagnes, annonces, créatifs
ads_readlire les dépenses et les résultats
business_managementlire la structure du business
pages_read_engagementdès qu'un créatif référence une page

Les trois pièges de cette étape

Piège 1

Un jeton sans actif affecté ne sert à rien. Générer le jeton ne donne aucun droit. Il faut ensuite, séparément, affecter chaque actif (compte publicitaire, pixel, page) au System User avec le bon niveau d'accès. Symptôme classique : l'API renvoie une liste vide ou (#200) Provide valid app ID alors que le jeton est parfaitement valide. On cherche le problème du mauvais côté pendant une heure.

Piège 2

Un seul System User pour toute la boîte. La tentation est d'en créer un par projet. C'est une erreur : chaque nouveau System User redemande une validation, et on finit avec cinq jetons dont personne ne sait lequel fait quoi. Un jeton maître, et pour un nouvel actif on l'affecte au System User existant. Rien à revalider, rien à redemander.

Piège 3

Le nom d'une campagne ne dit pas où elle est facturée. Une campagne peut porter le nom d'une marque et être débitée sur le compte publicitaire d'une autre. Avant toute lecture de dépense, vérifier le act_<id> réel, jamais le nom affiché.

Vérifier que le branchement marche

Trois appels, dans cet ordre. Si l'un échoue, inutile d'aller plus loin.

# 1. le jeton est-il vivant, et pour qui ?
curl -s "https://graph.facebook.com/v21.0/me?access_token=$TOKEN"

# 2. voit-il le compte publicitaire ?
curl -s "https://graph.facebook.com/v21.0/me/adaccounts\
?fields=name,account_status&access_token=$TOKEN"

# 3. voit-il le pixel ?
curl -s "https://graph.facebook.com/v21.0/$BUSINESS_ID/adspixels\
?fields=name,id&access_token=$TOKEN"
À figer dès le départ

Garder la version d'API (v21.0) explicite dans le code. Sans numéro, Meta sert la plus ancienne encore supportée, et le comportement de ton système change tout seul le jour où elle est retirée. Sans que rien ne te prévienne.

03Le pixel et le gestionnaire d'évènements

Poser le pixel

Le code de base va sur toutes les pages, y compris celles qu'on croit hors funnel : page de remerciement, page de réservation, pages légales. Une page sans pixel est un trou d'attribution qu'on ne verra jamais, parce qu'un trou ne produit aucune erreur.

Deux marchés, deux publics, deux campagnes distinctes → deux pixels distincts. Mélanger deux audiences dans un même pixel apprend à l'algorithme à chercher un profil moyen qui n'existe pas.

Ce que le gestionnaire d'évènements dit, et ce qu'il ne dit pas

L'écran afficheCe que ça veut vraiment dire
un évènement reçuMeta l'a accepté. Pas qu'il est exploitable.
« qualité de correspondance » sur 10calculé sur les champs envoyés, non exposé par l'API
un volume qui décollesouvent un envoyeur qui rejoue, pas une hausse réelle
un diagnostic « paramètre non valide »à lire en entier : il donne la valeur fautive
Le réflexe qui fait gagner des heures

Utiliser les évènements de test avant de croire quoi que ce soit. L'onglet « Test des évènements » donne un test_event_code : on l'ajoute au payload, l'évènement arrive dans une vue isolée, et il ne pollue pas les données de production. C'est le seul moyen de valider un format sans salir l'historique.

04Envoyer ses conversions serveur

Le principe : le navigateur envoie ce qu'il voit, le serveur envoie ce qui se passe après le clic (un rendez-vous honoré, une vente encaissée). Les deux se rejoignent par un event_id commun, qui sert à dédupliquer.

POST https://graph.facebook.com/v21.0/<PIXEL_ID>/events?access_token=<TOKEN>

{
  "data": [{
    "event_name": "Purchase",
    "event_time": 1753689600,        ← date de l'ACTION, pas de l'envoi
    "event_id": "vente-4821",        ← la clé de déduplication
    "action_source": "system_generated",
    "user_data": {
      "em":  "<sha256 de l'email en minuscules>",
      "ph":  "<sha256 du tél., chiffres seuls avec indicatif>",
      "fn":  "<sha256 du prénom en minuscules>",
      "fbc": "fb.1.<ms>.<identifiant de clic>",   ← EN CLAIR
      "fbp": "fb.1.<ms>.<aléatoire>",             ← EN CLAIR
      "client_ip_address": "1.2.3.4",             ← EN CLAIR
      "client_user_agent": "Mozilla/5.0 …"        ← EN CLAIR
    },
    "custom_data": { "value": 2500, "currency": "EUR" }
  }]
}
L'erreur la plus fréquente

Ce qui se hache et ce qui ne se hache pas. Les données personnelles en SHA‑256 minuscules sans espaces, mais les identifiants techniques (fbc, fbp, IP, navigateur) en clair. Hacher ces quatre-là revient à les jeter, et aucun message d'erreur ne te le dira.

05Publier des annonces par l'API

La structure, du haut vers le bas

Campagne         objectif + budget (si CBO)      /act_<id>/campaigns
  └── Ensemble    ciblage, budget, optimisation   /act_<id>/adsets
        └── Annonce      pointe vers un créatif   /act_<id>/ads
              └── Créatif   textes, visuels, lien /act_<id>/adcreatives
Non négociable

Toujours créer en PAUSED, à chaque étage. Une annonce créée active dépense pendant que tu vérifies encore qu'elle est correcte. On active à la fin, quand tout est relu.

L'ordre qui évite les surprises

  1. Créer le créatif d'abord, séparément. C'est l'étage qui échoue le plus (visuel refusé, réglage déprécié, page non affectée), autant le découvrir avant d'avoir créé une campagne vide.
  2. Créer campagne → ensemble → annonce, en PAUSED.
  3. Relire ce que l'API a réellement créé, pas ce que tu crois avoir envoyé.
  4. Activer.

Ce qui casse une création de créatif

Le budget se pose à un seul étage

Soit sur la campagne (CBO), soit sur l'ensemble. Jamais les deux : Meta accepte la contradiction sans broncher et applique celui qu'il veut.

06Les 9 règles qui ne se négocient pas

1. Le nom d'un évènement porte l'historique

Une campagne optimise sur un nom d'évènement. Le renommer remet son apprentissage à zéro. Quand on reprend un système existant, on rejoue exactement les mêmes noms, même s'ils sont mal fichus.

Corollaire : si on renomme quand même, le faire en une seule fois, cron arrêté. Sinon la salve qui part avant le renommage et celle qui part après comptent double. Vécu : 57 évènements doublés sur 35 rendez-vous, parce que le renommage est tombé entre deux passages du cron espacés d'une heure.

2. L'audience se déduit d'un identifiant, jamais d'un nom

Deux évènements du calendrier peuvent porter le même libellé et viser deux publics différents. Router sur le nom, c'est ranger des prospects dans le mauvais pixel, ce qui apprend à l'algorithme à chercher les mauvaises personnes.

Table identifiant → (audience, étape) en dur, et un identifiant inconnu est ignoré et signalé, jamais rangé au hasard. Le sens de l'erreur compte : un prospect manquant ralentit la campagne, un prospect mal rangé l'abîme.

3. Une macro publicitaire qu'on écrit à la main n'existe pas

{{fbclid}} n'est pas une macro Meta. Meta ajoute l'identifiant de clic tout seul. Écrite à la main, elle n'est jamais remplacée, arrive en toutes lettres sur la page, et le pixel la recopie dans un cookie qui vit 90 jours.

Les seules macros que Meta remplace : {{ad.id}}, {{adset.id}}, {{campaign.id}}, {{ad.name}}, {{adset.name}}, {{campaign.name}}, {{placement}}, {{site_source_name}}.

Et le filtre doit décoder l'URL en boucle. La même macro revient encodée, souvent plusieurs fois au fil des redirections :

{{fbclid}}  →  %7B%7Bfbclid%7D%7D  →  %257B%257B…  →  %2525252525257B…

Chercher {{ en clair laisse passer toutes les variantes encodées.

4. Un champ rempli d'une valeur morte vaut moins qu'un champ vide

Il fait croire à la plateforme qu'elle a de quoi travailler. Trois cas à filtrer :

5. Une tâche vide n'est pas une tâche remplie

La règle la plus importante de ce guide

Elle s'est refermée trois fois le même jour, à trois endroits différents du système.

Les outils de rendez-vous créent le compte rendu dès que l'appel est programmé, vide, et le commercial le remplit après coup. Entre les deux, un test naïf conclut à tort :

Test naïfCe qu'il conclutLa vérité
all(motif == "NO_SHOW") sur une tâche vide« il est venu » → envoie un Showon ne sait pas encore
if not completed → NoShow« il est absent » → marque absenton ne sait pas encore
statut « réservé » toujours actif« prépare-le » → email de préparationl'appel a déjà eu lieu

Toujours vérifier que le compte rendu porte QUELQUE CHOSE (un motif ou un résultat) avant de trancher. Sinon on attend le tour suivant.

Coût réel de cette erreur : 13 rendez-vous ont reçu à la fois un « Show » et un « NoShow », soit près d'un tiers des Show envoyés. Meta garde le premier reçu, donc c'est le faux qui est resté.

6. Ce qui compte, c'est la date de l'action, pas celle où on l'apprend

Une réservation compte le jour où le prospect a pris son créneau. Un appel compte le jour où il a lieu. Ne jamais dater un évènement du moment où le cron l'a découvert.

Corollaire pour les libellés de colonnes : « Calls réservés » se lit comme « réservations du jour » alors que la colonne compte les appels qui ont lieu ce jour-là. Un appel pris lundi pour jeudi compte le jeudi. Nommer les colonnes sans ambiguïté.

7. La plateforme ne fusionne pas deux évènements de même identifiant : elle garde le premier

Conséquence directe : ne jamais envoyer une version pauvre d'un évènement en espérant l'enrichir plus tard. Si un Purchase part sans montant, le Purchase chiffré qui arrive une heure après est jeté.

C'est pour ça qu'un webhook temps réel ne doit jamais envoyer l'évènement de vente : seul le système qui connaît le montant l'envoie.

8. On ne met un prix que là où il y a eu un paiement

La plateforme réclame prix et devise sur les évènements de réservation, en promettant du ROAS. C'est un piège de reporting. Quelqu'un qui réserve un appel n'a rien payé. Inventer une valeur estimée revient à déclarer du chiffre d'affaires qui n'existe pas, et le retour sur dépenses affiché devient illisible.

La valeur va sur l'achat, et nulle part ailleurs.

9. Un service sans état rejoue sa fenêtre à chaque réveil

Une fonction serverless (Cloudflare, Lambda, Vercel) n'a aucune mémoire entre deux appels. Si elle lit « les évènements des 3 derniers jours » et les envoie, elle renvoie les mêmes à chaque invocation.

Vécu : un webhook appelé plusieurs fois par jour envoyait 99 évènements à chaque fois, tous déjà partis. Le pixel affichait 1013 réservations par jour pour une réalité de 16, avec un trafic parfaitement stable. Les conversions n'étaient pas doublées (même event_id, la plateforme déduplique), mais les statistiques devenaient illisibles, au point de faire soupçonner l'ancienne agence à tort.

Tout envoyeur sans état a besoin de son propre journal, partagé et durable, avec la même clé que les autres envoyeurs. Et deux garde-fous dans le sens qui protège les conversions : ne noter qu'après un envoi réussi, et en cas de journal illisible, renvoyer plutôt que de perdre.

Le signal qui doit alerter

Un volume de conversions qui décolle alors que le trafic ne bouge pas. Toujours comparer les deux courbes avant d'aller chercher un coupable ailleurs.

07Corriger les paramètres d'URL d'annonces existantes

Le piège d'entrée

Ça répond oui et ça ne fait rien

Meta refuse de modifier url_tags sur une annonce ou un créatif existant. L'API répond success: true et ne change rien. Ni sur l'annonce, ni sur le créatif. On croit avoir corrigé, on n'a rien fait.

La seule voie : créer un nouveau créatif et le rattacher.

La recette qui marche

1. Lire le créatif ACTUEL en entier
   object_story_spec + asset_feed_spec
   + degrees_of_freedom_spec + url_tags

2. Nettoyer ce qui doit l'être (paramètres, lien de destination)

3. Retirer image_url (lecture seule) et tout réglage déprécié
   standard_enhancements fait échouer la création

4. POST vers /adcreatives avec la spec complète

5. POST sur l'annonce : creative = {creative_id: <nouveau>}

6. ATTENDRE, puis vérifier

Deux erreurs qui coûtent cher

Cloner depuis le seul identifiant de publication perd les variantes. Un créatif dynamique porte plusieurs textes, titres et descriptions. Un clone simplifié les efface, et l'annonce perd son optimisation. Toujours recopier le créatif entier, et comparer le nombre de variantes avant et après, annonce par annonce.

Vécu, et ça m'a coûté 14 annonces

La propagation n'est pas immédiate. J'ai corrigé 40 annonces, vérifié « 40/40 propres », puis enchaîné sur un second correctif en relisant l'état — qui affichait encore l'ancien créatif. J'ai donc recopié les anciens paramètres et réintroduit le bug sur 14 annonces. Ma vérification suivante est passée trop tôt et n'a rien vu.

Vérifier après propagation, en comparant chaque créatif à celui qu'on attend, pas en relisant un champ qui peut être en cache. Boucler avec attente jusqu'à ce que l'identifiant servi soit celui attendu.

Le risque à peser avant de se lancer

Remplacer un créatif remet l'annonce en revue et peut relancer la phase d'apprentissage. Avant d'y aller, chercher si l'opération a déjà été faite récemment sur ce compte sans casse : c'est le meilleur indicateur. Et sauvegarder tous les anciens identifiants de créatif, le retour arrière tient alors en une commande.

08Remonter un score de correspondance

Le score sur 10 n'est pas exposé par l'API, il n'existe que dans l'interface. Mais il est produit par la composition de ce qu'on envoie, qui elle se mesure.

ChampDifficultéRemarque
email, téléphone, prénom, nomfaciledoivent être à 100 %, sinon chercher pourquoi
identifiant client, pays, villefacilele pays se déduit de l'indicatif téléphonique
identifiant de navigateurmoyendépend du cookie du visiteur
identifiant de clicmoyenle vrai levier : macros et périmés le plombent
adresse IP + navigateurle gros levier serveurvoir ci-dessous

Récupérer l'adresse IP sur des évènements serveur

Le problème : les évènements partent après coup, depuis un cron qui n'a jamais vu le visiteur. Et l'outil de rendez-vous n'expose ni l'IP ni le navigateur.

La solution est un pont par le cookie de navigateur, le seul identifiant que le navigateur du visiteur et l'outil de rendez-vous partagent :

Le visiteur arrive sur la page de réservation
   → son navigateur envoie SON COOKIE au serveur
   → le serveur y ajoute l'IP et le navigateur qu'IL voit
   → on stocke la paire, courte durée

Plus tard, à l'envoi de la conversion
   → on retrouve l'IP par le cookie du rendez-vous

Deux règles de sécurité, non négociables :

Ces deux champs ne se hachent pas, Meta les veut en clair.

Deux suggestions à refuser

L'identifiant de connexion réseau social (+9 %) suppose un bouton « se connecter avec » dans le funnel, juste avant la réservation. La friction coûte plus que le gain.

Code postal, date de naissance, région (+8 % chacun) : les demander dans le formulaire fait chuter le taux de réservation bien plus que ça ne rapporte.

09Lire les rapports sans se faire piéger

Toujours borner la période à partir de la bascule

Une fenêtre de 7 jours qui chevauche un changement de système mélange deux sources et affiche des volumes absurdes. Vu : 708 réservations affichées pour une réalité de 40 par jour.

Avant toute lecture, savoir depuis quand le système actuel tourne, et borner la période là-dessus.

L'écart de fuseau est normal et ne se réconcilie pas

Un dashboard qui compte en heure locale et une plateforme qui compte en UTC ne tomberont jamais d'accord sur une journée isolée. Mesuré : 5 % des réservations changent de jour selon le référentiel. Sur une semaine ça s'annule. Ne pas chasser ce fantôme.

Le compteur d'acceptation n'est pas le code de retour

Une réponse HTTP 200 ne veut pas dire que tout a été accepté. La réponse porte un nombre d'évènements reçus. S'il est inférieur à ce qu'on a envoyé, ne rien marquer comme envoyé : le tour suivant réessaiera, et la déduplication par identifiant évite les doublons. Sinon les refusés disparaissent en silence.

10Vérifier que la chaîne dit la vérité

Une chaîne publicitaire ne s'arrête pas au pixel. Le statut affiché dans le CRM pilote les emails, et un statut faux envoie le mauvais message à un vrai client.

La méthode de contrôle

  1. Prendre la source de vérité (l'outil de rendez-vous), sur une fenêtre courte
  2. Calculer ce que chaque système DEVRAIT afficher
  3. Lire ce qu'il affiche vraiment
  4. Ne conclure au bug qu'après avoir vérifié sa propre logique de comparaison

Ce dernier point compte : sur 7 écarts trouvés, il a fallu ouvrir chaque cas pour distinguer les vrais bugs de mes propres approximations.

Ce que ce contrôle a trouvé

La règle de classement d'une vente

Le marché suit le lead, pas le montant. Si le prospect est entré par le funnel A, sa vente est du marché A quel que soit le prix signé. Un seuil de prix se périme au premier changement de grille tarifaire, et personne ne s'en aperçoit.

Rapprocher une vente de son lead demande une cascade : email, puis téléphone (sur les derniers chiffres, pour ignorer les indicatifs), puis nom normalisé. Le nom est indispensable : les formulaires de vente internes ne demandent souvent pas l'email.

11La checklist avant de toucher à un compte


Ce qu'on ne peut pas faire, et qu'il faut dire

Rester honnête sur les angles morts plutôt que de les taire :