# Suivi côté serveur sans fuite de données personnelles : le hashage SHA-256 expliqué

> OpenAI n'accepte emails, téléphones et noms qu'en SHA-256. Règles de normalisation, extrait Node.js, et pourquoi un hash reste une donnée personnelle.

Canonical: https://convrail.com/fr/blog/tracking-serveur-sans-fuite-donnees-personnelles/

Le suivi côté serveur envoie des identifiants client à une plateforme publicitaire pour qu'elle rapproche les conversions des utilisateurs. La Conversions API d'OpenAI n'accepte emails, numéros de téléphone, identifiants externes et noms que sous forme de hashes SHA-256 normalisés, et quelques champs comme l'adresse IP en clair. Le hashage empêche la valeur brute de sortir de votre infrastructure, mais un hash reste une donnée personnelle au sens du RGPD : consentement et contrat de sous-traitance restent obligatoires.

## Ce qu'est l'advanced matching, et pourquoi il touche aux données personnelles

Un pixel navigateur identifie un visiteur par ce que le navigateur transporte : un identifiant de clic dans l'URL, un cookie first-party. Quand ce contexte manque (le script a été bloqué, le cookie a expiré, la commande a été confirmée par un webhook plutôt que par une page), la plateforme publicitaire a besoin d'un autre moyen de relier la conversion à un utilisateur qu'elle connaît. L'advanced matching est cet autre moyen : le marchand envoie des identifiants que la plateforme peut comparer à ses propres enregistrements.

Les identifiants en question sont ceux que votre boutique détient déjà sur chaque commande : email, numéro de téléphone, identifiant client, nom, adresse. Ce sont des données à caractère personnel. Le problème d'ingénierie est de les rendre utiles au rapprochement sans les expédier en clair à un tiers ; le problème juridique est de les traiter licitement même une fois hashées. Cet article couvre les deux, avec la [Conversions API d'OpenAI](https://developers.openai.com/ads/conversions-api) comme cas concret. Le montage global à deux couches est décrit dans [Pixel OAIQ ou Conversions API](/fr/blog/pixel-oaiq-ou-conversions-api/). Les citations de la documentation d'OpenAI et de Shopify sont traduites par nous ; celles du RGPD reprennent le texte officiel français.

## Quels champs OpenAI accepte hashés, et lesquels en clair

L'objet `user` d'un événement Conversions API comporte deux sortes de champs. Chaque champ est facultatif ; plus vous en envoyez, meilleur est le rapprochement, et plus vous devez de diligence.

| Champ | Forme | Règle de la référence |
| --- | --- | --- |
| `emails_sha256` | Liste de hashes SHA-256 | « hashes SHA-256 d'adresses email normalisées » : espaces retirés aux extrémités, minuscules |
| `phone_numbers_sha256` | Liste de hashes SHA-256 | « 8 à 15 chiffres après avoir retiré un `+` initial, les zéros initiaux, les espaces, les parenthèses, les points et les tirets » |
| `external_ids_sha256` | Liste de hashes SHA-256 | « hashes SHA-256 d'identifiants client stables et pseudonymes » |
| `first_names_sha256` | Liste de hashes SHA-256 | « prénoms en minuscules après retrait des espaces et de la ponctuation ASCII » |
| `last_names_sha256` | Liste de hashes SHA-256 | « noms en minuscules après retrait des espaces et de la ponctuation ASCII » |
| `ip_address` | Clair | « adresse IPv4 ou IPv6 valide » |
| `user_agent` | Clair | « chaîne user agent non vide provenant du client » |
| `countries`, `regions`, `cities`, `postal_codes` | Clair, listes | Valeurs texte brutes, jusqu'à 3 valeurs chacune |
| `obref` | Clair | « référence navigateur opaque issue du cookie `__obref` du pixel » |
| `android_advertising_id` | Clair | « identifiant publicitaire Google Android (GAID) brut au format UUID » |

Un champ hashé est une liste, donc un client dont on connaît deux emails peut être rapproché par l'un ou l'autre. Chaque hash doit être la représentation hexadécimale minuscule du condensat SHA-256 : 64 caractères, `0-9a-f`. Convrail valide cette forme avant tout appel réseau et rejette tout le reste.

Notez ce qui est absent : il n'existe aucun champ `email`, `phone`, `first_name` ni `customer_id` en clair. Une charge utile qui en contient un est au mieux malformée, au pire une fuite de données. La [référence de la balise image](https://developers.openai.com/ads/image-tag) énonce le même principe côté navigateur : « Ne mettez pas de données personnelles, de secrets, d'identifiants de session, d'identifiants client ou d'identifiants de commande dans un paramètre de requête ».

## Les règles de normalisation, avec avant et après

Le hashage est déterministe : la même entrée donne toujours la même sortie, et une différence d'un caractère donne une sortie complètement différente. C'est ce qui rend le rapprochement possible, et c'est aussi pourquoi la normalisation compte plus que la fonction de hashage elle-même. Si vous hashez `Jane.Doe@example.com` et qu'OpenAI détient le hash de `jane.doe@example.com`, les deux chaînes de 64 caractères n'ont rien en commun et le rapprochement échoue en silence.

### Email

| Valeur brute | Normalisée | SHA-256 |
| --- | --- | --- |
| ` Jane.Doe@Example.com ` | `jane.doe@example.com` | `86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d` |
| `JANE.DOE@EXAMPLE.COM` | `jane.doe@example.com` | identique à la ligne précédente |

Espaces retirés, minuscules, hash. Ne retirez pas les points ni les suffixes après `+` de la partie locale ; la référence ne le demande pas, et le faire produirait des hashes qu'OpenAI ne peut pas rapprocher.

### Numéro de téléphone

| Valeur brute | Étapes | Normalisée | SHA-256 |
| --- | --- | --- | --- |
| `+33 6 12 34 56 78` | retirer `+`, retirer les espaces | `33612345678` | `8a3e7886c9335e82e02299fa3e87b46e2de3b0c63d56003e30a5029394a47661` |
| `(+33) 6.12.34.56.78` | retirer les parenthèses, le `+`, les points | `33612345678` | identique à la ligne précédente |
| `06 12 34 56 78` | retirer les espaces, retirer le zéro initial | `612345678` | `d500e1b5b11d4a3049f3a6761ac1720f7d33ffc18160dc9ca1a68cbcd1948d9c` |

La troisième ligne est le piège. La règle documentée retire les zéros initiaux, donc un numéro français écrit au format national perd son zéro de tête et finit en 9 chiffres sans indicatif pays. C'est une chaîne valide de 8 à 15 chiffres, OpenAI l'acceptera, et elle ne correspondra jamais au `33612345678` du même client venu d'une autre source. La règle est une normalisation de format, pas une conversion au format international. Faites la conversion vous-même avant de l'appliquer : stockez les numéros en E.164 (`+33612345678`) dès la saisie, en résolvant l'indicatif grâce au pays de la commande, puis hashez le résultat de la règle appliquée à cette forme.

### Noms

| Valeur brute | Normalisée | SHA-256 |
| --- | --- | --- |
| `Jean-Pierre` | `jeanpierre` | `fbcc8fa66b9a007dc3649ee989950d38f12ed91b9f18a3d215b04ff77546dca0` |
| ` Doe ` | `doe` | `799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f` |
| `Zoë` | `zoë` | `2752b88686847fa5c86f47b94ce652b7b3f22a91c37617d451a4db9afa431450` |

Minuscules, retrait des espaces et de la ponctuation ASCII (le tiret de Jean-Pierre disparaît, l'apostrophe de O'Brien disparaît). Les lettres accentuées ne sont pas de la ponctuation ASCII et restent telles quelles ; ne translittérez pas `ë` en `e`. Hashez les octets UTF-8.

### Identifiant externe

Tout identifiant stable et pseudonyme que votre système utilise déjà pour le client, comme un identifiant client Shopify. `customer-7781` donne `91d8dedaef47ad2016875e1ecbb6f01c00bba531c0d4c3c5661ed3129ae381b0`. Choisissez un format et ne le changez jamais, sinon chaque rapprochement passé se casse.

## Une implémentation Node.js

Toute la normalisation tient en quelques lignes. L'étape téléphone suppose que le numéro est déjà en E.164, pour la raison donnée plus haut.

```js
import { createHash } from 'node:crypto';

const sha256 = (value) => createHash('sha256').update(value, 'utf8').digest('hex');

export function hashEmail(raw) {
  const normalized = raw.trim().toLowerCase();
  return normalized ? sha256(normalized) : null;
}

export function hashPhone(e164) {
  // Règle de la référence Conversions API : retirer le + initial, les zéros initiaux,
  // les espaces, parenthèses, points et tirets ; conserver 8 à 15 chiffres.
  const digits = e164
    .replace(/^\+/, '')
    .replace(/[\s().-]/g, '')
    .replace(/^0+/, '');
  if (!/^\d{8,15}$/.test(digits)) return null;
  return sha256(digits);
}

export function hashName(raw) {
  const normalized = raw
    .toLowerCase()
    .replace(/\s/g, '')
    .replace(/[!-\/:-@\[-`{-~]/g, ''); // ponctuation ASCII seulement, les accents survivent
  return normalized ? sha256(normalized) : null;
}

export function buildUser(order) {
  const user = {
    emails_sha256: [hashEmail(order.email)].filter(Boolean),
    phone_numbers_sha256: [hashPhone(order.phoneE164)].filter(Boolean),
    external_ids_sha256: [sha256(String(order.customerId))],
    ip_address: order.clientIp,
    user_agent: order.userAgent,
    countries: [order.shippingCountry].filter(Boolean),
  };
  return Object.fromEntries(Object.entries(user).filter(([, v]) => v && v.length));
}
```

Renvoyer `null` pour une entrée vide ou malformée, puis la filtrer, compte : hasher une chaîne vide produit un condensat de 64 caractères parfaitement valide en apparence, qui correspond à tous les autres champs vides du monde. N'envoyez jamais le hash de rien.

## Pourquoi hasher n'est pas anonymiser

C'est le point que les marchands ratent le plus souvent, en général parce qu'un fournisseur leur a dit « nous n'envoyons que des données hashées, donc le RGPD ne s'applique pas ». Il s'applique.

Le [RGPD](https://eur-lex.europa.eu/eli/reg/2016/679/oj/fra) définit à l'article 4 point 1 les données à caractère personnel comme « toute information se rapportant à une personne physique identifiée ou identifiable ». L'article 4 point 5 définit la pseudonymisation comme « le traitement de données à caractère personnel de telle façon que celles-ci ne puissent plus être attribuées à une personne concernée précise sans avoir recours à des informations supplémentaires ». Le hash SHA-256 d'un email est exactement cela : seul, il ne désigne personne, mais quiconque détient l'email (vous, OpenAI si la personne est un utilisateur ChatGPT avec cette adresse, n'importe qui disposant d'une liste d'adresses à hasher et comparer) peut de nouveau l'attribuer à la personne. Le considérant 26 trace la ligne : les données pseudonymisées qui pourraient être attribuées à une personne physique par le recours à des informations supplémentaires « devraient être considérées comme des informations concernant une personne physique identifiable », alors que le règlement « ne s'applique, par conséquent, pas au traitement de telles informations anonymes ».

Conséquences pour une boutique qui envoie des identifiants hashés à une plateforme publicitaire :

- **Vous avez toujours besoin d'une base légale.** Pour une mesure publicitaire impliquant un tiers, c'est en pratique le consentement recueilli par votre bandeau, ou une autre base que votre conseil a documentée. Le hashage change le profil de risque, pas la catégorie juridique. Convrail enregistre les événements des visiteurs qui ont refusé le suivi comme `skipped_consent` et ne les envoie jamais.
- **Vous devez toujours informer.** Votre politique de confidentialité doit nommer le traitement (mesure des conversions avec OpenAI), les données concernées (email et téléphone hashés, adresse IP, user agent) et le destinataire.
- **Les droits de suppression et d'accès s'appliquent toujours.** C'est pourquoi la mécanique de suppression décrite plus bas opère sur des hashes.

Là où le hashage aide, et il aide beaucoup : une charge utile qui fuit, un journal compromis, une requête interceptée ou une intégration mal configurée exposent des condensats, pas des adresses. Le rayon d'impact d'une erreur est bien plus petit. C'est une bonne raison de hasher ; ce n'est pas une raison de sauter le consentement.

## Responsable du traitement, sous-traitant, et ce que chacun doit

L'article 4 point 7 définit le responsable du traitement comme celui qui « détermine les finalités et les moyens du traitement », et l'article 4 point 8 le sous-traitant comme celui qui « traite des données à caractère personnel pour le compte du responsable du traitement ». Quand vous connectez votre boutique à un connecteur de suivi :

- **Le marchand est le responsable du traitement.** Vous décidez que les données de commande serviront à mesurer les conversions ChatGPT Ads, et vous décidez d'utiliser un outil donné pour le faire.
- **Le connecteur est un sous-traitant.** Convrail reçoit les webhooks de commande, hashe les identifiants, met les événements en lots et les envoie à OpenAI sur vos instructions. L'article 28 paragraphe 3 exige que ce traitement soit « régi par un contrat ou un autre acte juridique », l'accord de traitement des données que vous devriez lire avant d'installer toute application de suivi, la nôtre comprise.
- **Le rôle d'OpenAI pour les données de rapprochement est défini dans ses propres conditions publicitaires**, que vous acceptez en ouvrant un compte Ads. Lisez-les ; nous ne les reformulons pas ici.

Ce qu'un sous-traitant vous doit en pratique, et ce qu'il faut vérifier chez tout connecteur : hasher dès la réception pour que les valeurs en clair ne soient jamais stockées (Convrail hashe emails et numéros de téléphone à l'instant où un webhook arrive et ne conserve jamais la valeur en clair), chiffrer les identifiants de connexion au repos (AES-256-GCM dans le cas de Convrail), et supprimer sur demande.

## Les demandes de suppression, appliquées par hash

Shopify rend le parcours de suppression concret avec trois webhooks obligatoires décrits dans sa [documentation de conformité aux lois sur la vie privée](https://shopify.dev/docs/apps/build/compliance/privacy-law-compliance) :

| Webhook | Quand Shopify l'envoie | Ce que l'application doit faire |
| --- | --- | --- |
| `customers/data_request` | Quand un client demande ses données au propriétaire de la boutique | Fournir les données du client au propriétaire de la boutique, à partir des identifiants de ressources de la charge utile |
| `customers/redact` | « Si un client n'a pas passé de commande au cours des six derniers mois, Shopify envoie la charge utile 10 jours après la demande de suppression. Sinon, la demande est retenue jusqu'à ce que six mois se soient écoulés » | Caviarder ou supprimer les données du client |
| `shop/redact` | « 48 heures après qu'un propriétaire de boutique a désinstallé votre application » | « effacer les données de cette boutique de votre base de données » |

Pour chacun, l'application doit « accomplir l'action dans les 30 jours suivant la réception de la demande » et accuser réception avec un statut de la série 200.

Le piège pour un connecteur qui ne stocke jamais de valeurs en clair : la charge utile de `customers/redact` contient l'email et l'identifiant du client en clair, et le connecteur ne détient que des hashes. La réponse consiste à hasher les identifiants entrants avec exactement la même normalisation qu'à la capture et à supprimer chaque événement dont `emails_sha256` ou `external_ids_sha256` correspond. C'est une raison de plus pour que la normalisation soit déterministe et ne change jamais : une boutique qui passerait discrètement d'un email nettoyé de ses espaces à un email non nettoyé serait incapable de retrouver, donc de supprimer, les enregistrements créés avant le changement. Convrail applique la suppression de cette façon et purge toute la boutique sur `shop/redact`.

## Comment le garde-fou de Convrail bloque les données personnelles en clair

Les règles de hashage ne valent que par leur application. Une refonte, un nouveau champ mappé depuis un webhook, un journal de débogage laissé actif : chacun de ces événements peut remettre un email en clair dans une charge utile des mois après la revue du code d'origine. Convrail place un garde-fou entre le constructeur de lots et le client HTTP, et il s'exécute sur chaque charge utile sortante sans exception.

Le garde-fou refuse d'envoyer un lot quand l'un des éléments suivants apparaît n'importe où dans la charge utile sérialisée :

- une chaîne correspondant à un motif d'email (une partie locale, un `@`, un domaine) ;
- une clé interdite comme `email`, `phone` ou `first_name`, quelle que soit sa position dans la structure ;
- un champ hashé dont la valeur n'est pas exactement 64 caractères hexadécimaux minuscules.

Une charge utile refusée n'atteint jamais le réseau. Le garde-fou lui-même est couvert par un test automatisé qui tente de faire passer des données personnelles en clair et doit échouer ; si un changement futur affaiblit le garde-fou, le test échoue et le changement n'est pas livré.

C'est la propriété à exiger de tout outil de suivi côté serveur : pas « nous hashons », qui est une déclaration d'intention, mais « les données personnelles en clair ne peuvent pas sortir, et voici le mécanisme qui l'impose ».

## Erreurs fréquentes

- **Hasher sans normaliser.** Des majuscules, un espace parasite et un format téléphonique national produisent chacun un hash qu'OpenAI ne peut pas rapprocher. Le hash est bon ; l'entrée était fausse.
- **Hasher des chaînes vides.** Un condensat de rien, valide en apparence, envoyé comme clé de rapprochement.
- **Appliquer la règle des zéros initiaux à des numéros nationaux.** `06 12 34 56 78` devient `612345678` et ne correspondra jamais à `33612345678`. Convertissez d'abord en E.164.
- **Translittérer les accents des noms.** La règle retire la ponctuation ASCII seulement ; `zoë` reste `zoë`.
- **Saler.** Un hash salé ne peut être rapproché par personne qui ignore le sel, ce qui annule l'objectif. L'advanced matching utilise du SHA-256 non salé par conception ; la protection de la vie privée vient du consentement et de la minimisation, pas du sel.
- **Considérer les données hashées comme hors du champ du RGPD.** Ce sont des données à caractère personnel pseudonymisées. Les obligations de consentement, d'information et de suppression demeurent.
- **Mettre des identifiants dans des URL.** La référence de la balise image interdit les données personnelles et les identifiants de commande dans les paramètres de requête ; il en va de même pour vos propres points de terminaison de relais et vos journaux.
- **Changer la normalisation plus tard.** Chaque hash passé devient impossible à rapprocher et, pire, impossible à supprimer.

## Et maintenant

Vérifiez ce que votre suivi actuel envoie en ouvrant une vraie charge utile et en y cherchant `@` ; si vous préférez qu'un garde-fou le fasse sur chaque lot, le [module de suivi des conversions de Convrail](/fr/tracking-conversions/) hashe dès la réception, bloque automatiquement les données personnelles en clair et applique les webhooks de suppression par hash.