Conversion API Meta dans PrestaShop : intégration côté serveur

Conversion API Meta dans PrestaShop : intégration côté serveur
ITP, bloqueurs de pub et refus de cookies font perdre 15 à 35 % des conversions au pixel Meta. Capture de fbp/fbc, normalisation SHA-256, déduplication par event_id, consentement et reprise sur échec : l'implémentation complète de la Conversions API dans PrestaShop.

Entre ITP, bloqueurs de pub et refus de cookies, le pixel Meta seul ne voit plus qu'une fraction de vos conversions. La Conversions API renvoie les événements depuis votre serveur PrestaShop — à condition de gérer correctement la déduplication, le hachage des identifiants et le consentement. Voici l'implémentation complète.

Pourquoi le pixel seul ne suffit plus

Le pixel Meta est un script navigateur. Il subit donc toutes les limitations du client : Safari ITP qui plafonne la durée de vie des cookies first-party posés en JavaScript, bloqueurs de publicité qui filtrent connect.facebook.net, refus de consentement, réseaux d'entreprise restrictifs.

Sur une boutique e-commerce classique, la perte de signal se situe généralement entre 15 et 35 % des conversions. Concrètement : un ROAS sous-évalué, des campagnes optimisées sur des données partielles et un algorithme de diffusion qui apprend mal.

La Conversions API (CAPI) répond à ce problème en envoyant les événements directement de votre serveur vers Meta, en HTTP. Aucun bloqueur ne s'interpose, aucune expiration de cookie ne s'applique. L'objectif n'est pas de remplacer le pixel mais de le doubler : les deux sources cohabitent et sont dédupliquées.

Architecture cible

  • Pixel navigateur : capte les identifiants Meta (_fbp, _fbc) et les événements de navigation
  • Serveur PrestaShop : émet les événements critiques (achat, ajout au panier, lead) avec les données client
  • Déduplication : un event_id partagé évite le double comptage

Étape 1 : préparer le dataset et le token

Dans le Gestionnaire d'événements Meta, votre pixel est désormais désigné comme un dataset. Son identifiant (dataset_id) est le même que l'ancien Pixel ID — il sert aux deux canaux.

Générez ensuite un token d'accès système depuis Paramètres du dataset → Conversions API → Générer un jeton d'accès. Ce token est un secret de production : il ne doit jamais figurer dans le dépôt Git.

# .env — jamais commité
META_DATASET_ID=123456789012345
META_CAPI_TOKEN=EAAG...
META_GRAPH_VERSION=v25.0

Meta a publié la v25.0 de la Graph API en février 2026 et retire les versions anciennes par vagues. Externalisez la version dans la configuration : une version dépréciée est l'une des causes les plus silencieuses d'arrêt d'ingestion des événements.

Étape 2 : capturer les identifiants côté navigateur

C'est le point le plus souvent bâclé. Les paramètres fbp et fbc sont ceux qui pèsent le plus dans l'attribution, et ils n'existent que côté client. Sans eux, votre CAPI enverra des événements mal rattachés.

  • _fbp : cookie first-party posé par le pixel, identifie le navigateur
  • _fbc : dérivé du paramètre d'URL fbclid au format fb.1.{timestamp}.{fbclid}

Le réflexe : persister ces deux valeurs dans la session PrestaShop dès l'arrivée du visiteur, pour qu'elles soient disponibles au moment de valider la commande — y compris si le tunnel dure plusieurs jours.

// Template du hook displayHeader
(function () {
    function readCookie(name) {
        var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
        return match ? match[2] : null;
    }

    var payload = {
        fbp: readCookie('_fbp'),
        fbc: readCookie('_fbc')
    };

    if (!payload.fbp && !payload.fbc) {
        return;
    }

    // Persiste les identifiants dans la session serveur
    navigator.sendBeacon('/module/metacapi/identifiers', new Blob(
        [JSON.stringify(payload)],
        { type: 'application/json' }
    ));
})();

Le contrôleur front qui reçoit ces valeurs se contente de les écrire dans le cookie de session PrestaShop. Pas de traitement, pas de stockage en base : ce sont des données de tracking à durée de vie courte.

Étape 3 : normaliser et hacher les données client

Meta impose un format strict pour les données personnelles : normalisation puis SHA-256. Une erreur de normalisation ne provoque aucune erreur d'API — l'événement est accepté mais ne correspond à personne. C'est un échec silencieux, et c'est pour cela qu'il faut isoler cette logique dans une classe testable.

<?php

declare(strict_types=1);

namespace Itroom\MetaCapi\Service;

/**
 * Normalise et hache les données utilisateur selon les règles Meta CAPI.
 */
final class UserDataNormalizer
{
    private const HASH_ALGO = 'sha256';

    /**
     * Champs à normaliser en minuscules puis hacher.
     */
    private const HASHED_FIELDS = ['em', 'fn', 'ln', 'ct', 'st', 'zp', 'country'];

    /**
     * @param array<string, string|null> $rawFields
     *
     * @return array<string, string>
     */
    public function normalize(array $rawFields): array
    {
        $normalized = [];

        foreach (self::HASHED_FIELDS as $field) {
            $value = $rawFields[$field] ?? null;

            if (null === $value || '' === trim($value)) {
                continue;
            }

            $normalized[$field] = $this->hash(mb_strtolower(trim($value)));
        }

        if (!empty($rawFields['ph'])) {
            $normalized['ph'] = $this->hash($this->normalizePhone($rawFields['ph']));
        }

        return $normalized;
    }

    /**
     * Format E.164 sans le signe « + » : uniquement des chiffres, indicatif inclus.
     */
    private function normalizePhone(string $phone): string
    {
        return ltrim(preg_replace('/\D+/', '', $phone) ?? '', '0');
    }

    private function hash(string $value): string
    {
        return hash(self::HASH_ALGO, $value);
    }
}

Les trois pièges classiques : hacher avant de passer en minuscules, oublier de couper les espaces, ou hacher deux fois une valeur déjà hachée par un module amont.

Ce qu'il ne faut surtout pas hacher

À l'inverse, quatre paramètres doivent être transmis en clair, sinon Meta ne peut plus les exploiter pour l'attribution :

  • client_ip_address : l'IP réelle du visiteur
  • client_user_agent : le User-Agent complet du navigateur
  • fbp et fbc : les identifiants Meta récupérés à l'étape 2

Attention à l'IP derrière un reverse proxy ou un CDN : $_SERVER['REMOTE_ADDR'] renverra l'adresse du proxy. Utilisez l'en-tête transmis par votre infrastructure (X-Forwarded-For) après l'avoir validée.

Étape 4 : envoyer l'événement depuis PrestaShop

Le client HTTP reste volontairement simple : construire le payload, l'envoyer, journaliser l'échec sans jamais bloquer le tunnel de commande.

<?php

declare(strict_types=1);

namespace Itroom\MetaCapi\Service;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class ConversionsApiClient
{
    private const ENDPOINT = 'https://graph.facebook.com/%s/%s/events';

    public function __construct(
        private readonly HttpClientInterface $httpClient,
        private readonly LoggerInterface $logger,
        private readonly string $datasetId,
        private readonly string $accessToken,
        private readonly string $graphVersion
    ) {
    }

    /**
     * Envoie un lot d'événements vers Meta.
     *
     * @param array<int, array<string, mixed>> $events
     *
     * @return bool True si Meta a accepté le lot
     */
    public function send(array $events): bool
    {
        if ([] === $events) {
            return true;
        }

        $url = sprintf(self::ENDPOINT, $this->graphVersion, $this->datasetId);

        try {
            $response = $this->httpClient->request('POST', $url, [
                'json' => [
                    'data' => $events,
                    'access_token' => $this->accessToken,
                ],
                'timeout' => 3,
            ]);

            if (200 === $response->getStatusCode()) {
                return true;
            }

            $this->logger->error('Meta CAPI rejected the batch', [
                'status' => $response->getStatusCode(),
                'body' => $response->getContent(false),
            ]);
        } catch (\Throwable $exception) {
            $this->logger->error('Meta CAPI request failed', [
                'message' => $exception->getMessage(),
            ]);
        }

        return false;
    }
}

Le timeout de 3 secondes est volontairement bas. Un appel sortant lent ne doit jamais retarder l'affichage de la page de confirmation de commande.

Déclenchement sur la validation de commande

Le hook actionValidateOrder est le point d'ancrage naturel de l'événement Purchase. Il est appelé une seule fois par commande, après création effective.

<?php

declare(strict_types=1);

/**
 * Émet l'événement Purchase vers la Conversions API.
 *
 * @param array<string, mixed> $params
 */
public function hookActionValidateOrder(array $params): void
{
    $order = $params['order'];
    $customer = $params['customer'];
    $address = new Address((int) $order->id_address_invoice);

    $userData = $this->normalizer->normalize([
        'em' => $customer->email,
        'fn' => $customer->firstname,
        'ln' => $customer->lastname,
        'ph' => $address->phone_mobile ?: $address->phone,
        'ct' => $address->city,
        'zp' => $address->postcode,
        'country' => Country::getIsoById((int) $address->id_country),
    ]);

    $userData['client_ip_address'] = Tools::getRemoteAddr();
    $userData['client_user_agent'] = (string) $_SERVER['HTTP_USER_AGENT'];
    $userData['fbp'] = $this->context->cookie->meta_fbp ?: null;
    $userData['fbc'] = $this->context->cookie->meta_fbc ?: null;

    $this->client->send([[
        'event_name' => 'Purchase',
        'event_time' => time(),
        'event_id' => 'purchase_' . $order->reference,
        'event_source_url' => $this->context->link->getPageLink('order-confirmation'),
        'action_source' => 'website',
        'user_data' => array_filter($userData),
        'custom_data' => [
            'currency' => $this->context->currency->iso_code,
            'value' => round((float) $order->total_paid, 2),
            'content_type' => 'product',
            'content_ids' => $this->extractProductIds($order),
        ],
    ]]);
}

Le array_filter final n'est pas cosmétique : envoyer une clé avec une valeur null dégrade la qualité de correspondance côté Meta. Mieux vaut omettre le champ.

Étape 5 : la déduplication, ou comment ne pas doubler son ROAS

Si le pixel et la CAPI envoient tous deux un Purchase sans coordination, Meta comptabilise deux conversions. La déduplication repose sur une règle simple : même event_name et même event_id sur les deux canaux, dans une fenêtre de 48 heures.

D'où le choix de 'purchase_' . $order->reference plutôt qu'un UUID aléatoire : la référence de commande est stable, disponible côté serveur comme côté template, et rejouable en cas de renvoi.

{* Template de la page de confirmation *}
<script>
fbq('track', 'Purchase', {
    currency: '{$currency|escape:'javascript'}',
    value: {$total_paid|floatval}
}, {
    eventID: 'purchase_{$order_reference|escape:'javascript'}'
});
</script>

Notez la casse : eventID côté pixel JavaScript, event_id côté API serveur. Une erreur de casse et la déduplication ne s'applique pas — sans le moindre message d'erreur.

Étape 6 : respecter le consentement

La CAPI n'échappe pas au RGPD. Un événement serveur contenant un e-mail haché et une adresse IP reste un traitement de données personnelles à finalité publicitaire : il nécessite le consentement de l'utilisateur.

Concrètement, le hook doit vérifier l'état du consentement publicitaire avant tout envoi, en s'appuyant sur la même source de vérité que votre bannière. Si vous avez déjà implémenté le Consent Mode v2, réutilisez le signal ad_user_data persisté en session plutôt que d'introduire une seconde logique de consentement.

if (!$this->consentChecker->isAdvertisingGranted()) {
    return;
}

Meta propose par ailleurs le champ data_processing_options pour les régimes de restriction (notamment la Californie). Il n'est pas requis en Europe, mais son absence est parfois signalée à tort comme un problème dans les audits de conformité.

Étape 7 : valider l'implémentation

Trois niveaux de vérification, du plus immédiat au plus structurel :

  • Test Events : ajoutez le paramètre test_event_code au payload et observez l'arrivée en temps réel dans le Gestionnaire d'événements. C'est le seul moyen de valider le format avant mise en production.
  • Déduplication : dans l'onglet Diagnostics, Meta indique le pourcentage d'événements dédupliqués. Un Purchase envoyé sur les deux canaux doit afficher un taux proche de 100 %.
  • Event Match Quality : score de 1 à 10 mesurant la capacité de Meta à rattacher vos événements à des comptes. Visez 8,5+ sur Purchase, 8+ sur AddToCart. En dessous de 6, il manque des paramètres dans user_data.

L'e-mail haché est de loin le paramètre le plus contributeur au score, suivi du téléphone. Sur une boutique avec beaucoup de commandes invité, remonter l'EMQ passe presque toujours par la récupération fiable de l'e-mail de facturation.

Fiabiliser avec une file de reprise

Un timeout réseau ne doit pas signifier une conversion perdue. Le pattern robuste : persister l'événement en base à l'échec, puis le rejouer via une commande Symfony planifiée.

<?php

declare(strict_types=1);

namespace Itroom\MetaCapi\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
    name: 'itroom:meta-capi:retry',
    description: 'Replays Meta CAPI events that failed to be delivered'
)]
final class RetryFailedEventsCommand extends Command
{
    private const BATCH_SIZE = 100;
    private const MAX_ATTEMPTS = 5;

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $pending = $this->repository->findPending(self::BATCH_SIZE, self::MAX_ATTEMPTS);

        if ([] === $pending) {
            return Command::SUCCESS;
        }

        foreach ($pending as $event) {
            $delivered = $this->client->send([$event->getPayload()]);
            $this->repository->markAttempt($event->getId(), $delivered);
        }

        $output->writeln(sprintf('%d event(s) processed', count($pending)));

        return Command::SUCCESS;
    }
}

La fenêtre de déduplication étant de 48 heures, un rejeu au-delà de ce délai risque de créer un doublon. Purgez donc les événements trop anciens plutôt que de les rejouer indéfiniment — d'où le plafond de tentatives.

Les erreurs fréquentes

Envoyer l'événement en synchrone dans le tunnel

Un appel HTTP sortant dans actionValidateOrder sans timeout court peut ajouter plusieurs secondes à la validation de commande. Timeout bas, échec silencieux journalisé, reprise asynchrone.

Oublier fbp et fbc

C'est la cause numéro un d'un EMQ médiocre malgré des données client complètes. Sans ces identifiants, Meta doit s'appuyer uniquement sur la correspondance probabiliste.

Confondre CAPI et GTM server-side

Les deux approches sont valables et ne s'excluent pas. Un tag Meta dans un conteneur GTM server-side vous évite d'écrire le client HTTP, mais vous perdez l'accès direct aux données de commande côté PHP — notamment pour les événements différés (remboursement, changement de statut). Sur une boutique PrestaShop, l'approche module reste la plus fiable pour Purchase.

Ne pas surveiller le score dans la durée

Un changement de thème, une refonte du tunnel ou une mise à jour de CMP suffisent à casser la capture de _fbp. Contrôlez l'EMQ au minimum une fois par semaine : la chute est toujours détectable avant que le ROAS ne s'effondre.

Conclusion

La Conversions API n'est pas une case à cocher : c'est une intégration qui touche au tunnel de commande, aux données personnelles et au consentement. Les quatre points qui font la différence entre une implémentation cosmétique et une implémentation qui récupère réellement du signal : la capture de fbp/fbc, la normalisation rigoureuse avant hachage, un event_id déterministe partagé avec le pixel, et une reprise sur échec.

Le reste est de la mesure. Test Events pour valider le format, taux de déduplication pour vérifier la cohabitation, Event Match Quality pour piloter la qualité dans la durée.

Besoin d'implémenter ou d'auditer la Conversions API sur votre boutique PrestaShop ? Contactez-moi pour en discuter !

Sources

Jonathan Le-Peru

Écrit par Jonathan Le-Peru

Développeur backend avec plus de 7 ans d'expérience, spécialisé dans la création de solutions e-commerce robustes avec Prestashop. Passionné par l'optimisation des performances et les bonnes pratiques de développement.