Server-side tracking sends customer identifiers to an ad platform so it can match conversions to users. The OpenAI Conversions API accepts emails, phone numbers, external IDs and names only as normalized SHA-256 hashes, and a few fields such as IP address in clear. Hashing prevents the raw value from leaving your infrastructure, but a hash is still personal data under GDPR: consent and a processor contract remain required.
What advanced matching is, and why it involves personal data
A browser pixel identifies a visitor by what the browser carries: a click identifier in the URL, a first-party cookie. When that context is missing (the script was blocked, the cookie expired, the order was confirmed by a webhook rather than a page), the ad platform needs another way to connect the conversion to a user it knows. Advanced matching is that other way: the merchant sends identifiers the platform can compare against its own records.
The identifiers in question are the ones your store already holds on every order: email, phone number, customer ID, name, address. They are personal data. The engineering problem is to make them useful for matching without shipping them in clear to a third party; the legal problem is to process them lawfully even after they are hashed. This article covers both, using the OpenAI Conversions API as the concrete case. The overall two-layer setup is described in OAIQ pixel vs Conversions API.
Which fields OpenAI accepts hashed, and which in clear
The user object of a Conversions API event has two kinds of fields. Every field is optional; the more you send, the better the matching, and the more you owe in diligence.
| Field | Form | Rule from the reference |
|---|---|---|
emails_sha256 | List of SHA-256 hashes | “SHA-256 hashes of normalized email addresses”: trim whitespace, lowercase |
phone_numbers_sha256 | List of SHA-256 hashes | “8-15 digits after removing a leading +, leading zeroes, whitespace, parentheses, periods, and hyphens” |
external_ids_sha256 | List of SHA-256 hashes | “SHA-256 hashes of stable, pseudonymous customer identifiers” |
first_names_sha256 | List of SHA-256 hashes | “Lowercase first names after removing whitespace and ASCII punctuation” |
last_names_sha256 | List of SHA-256 hashes | “Lowercase last names after removing whitespace and ASCII punctuation” |
ip_address | Clear | “Valid IPv4 or IPv6 address” |
user_agent | Clear | “Non-empty user agent string from the client” |
countries, regions, cities, postal_codes | Clear, lists | Raw string values, up to 3 values each |
obref | Clear | “Opaque browser reference from the Pixel’s __obref cookie” |
android_advertising_id | Clear | “Raw Android Google Advertising ID (GAID) in UUID format” |
A hashed field is a list, so a customer with two known emails can be matched on either. Each hash must be the lowercase hexadecimal representation of the SHA-256 digest: 64 characters, 0-9a-f. Convrail validates that shape before any network call and rejects anything else.
Note what is absent: there is no email, phone, first_name or customer_id field in clear. A payload that contains one is malformed at best and a data leak at worst. The image tag reference states the same principle for the browser side: “Don’t put personal data, secrets, session IDs, customer identifiers, or order identifiers in any query parameter”.
Normalization rules, with before and after
Hashing is deterministic: the same input always gives the same output, and a one-character difference gives a completely different output. That is what makes matching possible, and it is also why normalization matters more than the hash function itself. If you hash Jane.Doe@example.com and OpenAI holds the hash of jane.doe@example.com, the two 64-character strings share nothing and the match fails silently.
| Raw value | Normalized | SHA-256 |
|---|---|---|
Jane.Doe@Example.com | jane.doe@example.com | 86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d |
JANE.DOE@EXAMPLE.COM | jane.doe@example.com | same as above |
Trim, lowercase, hash. Do not strip dots or plus-suffixes from the local part; the reference does not ask for it and doing so would produce hashes OpenAI cannot match.
Phone number
| Raw value | Steps | Normalized | SHA-256 |
|---|---|---|---|
+33 6 12 34 56 78 | remove +, remove spaces | 33612345678 | 8a3e7886c9335e82e02299fa3e87b46e2de3b0c63d56003e30a5029394a47661 |
(+33) 6.12.34.56.78 | remove parentheses, +, periods | 33612345678 | same as above |
06 12 34 56 78 | remove spaces, remove leading zero | 612345678 | d500e1b5b11d4a3049f3a6761ac1720f7d33ffc18160dc9ca1a68cbcd1948d9c |
The third row is the trap. The documented rule removes leading zeros, so a French number written in national format loses its trunk zero and ends up as 9 digits without a country code. It is a valid 8-15 digit string, OpenAI will accept it, and it will never match the same customer’s 33612345678 from another source. The rule is a normalization of format, not a conversion to international format. Do the conversion yourself before applying it: store phone numbers in E.164 (+33612345678) at capture time, using the order’s country to resolve the prefix, and hash the rule’s output of that.
Names
| Raw value | Normalized | SHA-256 |
|---|---|---|
Jean-Pierre | jeanpierre | fbcc8fa66b9a007dc3649ee989950d38f12ed91b9f18a3d215b04ff77546dca0 |
Doe | doe | 799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f |
Zoë | zoë | 2752b88686847fa5c86f47b94ce652b7b3f22a91c37617d451a4db9afa431450 |
Lowercase, remove whitespace and ASCII punctuation (the hyphen in Jean-Pierre goes, the apostrophe in O’Brien goes). Accented letters are not ASCII punctuation and stay as they are; do not transliterate ë to e. Hash the UTF-8 bytes.
External ID
Any stable, pseudonymous identifier your system already uses for the customer, such as a Shopify customer ID. customer-7781 hashes to 91d8dedaef47ad2016875e1ecbb6f01c00bba531c0d4c3c5661ed3129ae381b0. Pick one format and never change it, or every past match breaks.
A Node.js implementation
The whole normalization fits in a few lines. The phone step assumes the number is already in E.164 for the reason given above.
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) {
// Rule from the Conversions API reference: drop the leading +, leading zeros,
// whitespace, parentheses, periods and hyphens; keep 8 to 15 digits.
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, ''); // ASCII punctuation only, accents survive
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));
}
Returning null for an empty or malformed input, then filtering it out, matters: hashing an empty string produces a perfectly valid-looking 64-character digest that matches every other empty field in the world. Never send the hash of nothing.
Why hashing is not anonymization
This is the part merchants most often get wrong, usually because a vendor told them “we only send hashed data, so GDPR does not apply”. It does.
The GDPR defines personal data in Article 4(1) as “any information relating to an identified or identifiable natural person”. Article 4(5) defines pseudonymisation as “the processing of personal data in such a manner that the personal data can no longer be attributed to a specific data subject without the use of additional information”. A SHA-256 hash of an email is exactly that: on its own it points to nobody, but anyone who holds the email (you, OpenAI if the person is a ChatGPT user with that email, anyone with a list of addresses to hash and compare) can attribute it to the person again. Recital 26 draws the line: pseudonymised data that could be attributed to a natural person by the use of additional information “should be considered to be information on an identifiable natural person”, while the regulation “does not therefore concern the processing of such anonymous information”.
Consequences for a store sending hashed identifiers to an ad platform:
- You still need a lawful basis. For advertising measurement involving a third party, that is in practice consent collected through your consent banner, or another basis your counsel has documented. Hashing changes the risk profile, not the legal category. Convrail records events from visitors who declined tracking as
skipped_consentand never sends them. - You still need to inform. Your privacy policy has to name the processing (conversion measurement with OpenAI), the data involved (hashed email and phone, IP address, user agent), and the recipient.
- Deletion and access rights still apply. Which is why the deletion mechanics below operate on hashes.
Where hashing does help, and it helps a lot: a leaked payload, a compromised log, an intercepted request or a misconfigured integration exposes digests, not addresses. The blast radius of a mistake is far smaller. That is a strong reason to hash; it is not a reason to skip consent.
Controller, processor, and what each owes
Article 4(7) defines the controller as the body which “determines the purposes and means of the processing of personal data”, and Article 4(8) the processor as the body which “processes personal data on behalf of the controller”. When you connect your store to a tracking connector:
- The merchant is the controller. You decide that order data will be used to measure ChatGPT Ads conversions, and you decide to use a given tool to do it.
- The connector is a processor. Convrail receives order webhooks, hashes identifiers, batches events and sends them to OpenAI on your instructions. Article 28(3) requires that processing to be “governed by a contract or other legal act”, the data processing agreement you should read before installing any tracking app, ours included.
- OpenAI’s role for the matching data is defined in its own advertising terms, which you accept when you open an Ads account. Read them; we do not restate them here.
What a processor owes you in practice, and what to check in any connector: hash on receipt so clear values are never stored (Convrail hashes emails and phone numbers the moment a webhook arrives and never persists the clear value), encrypt credentials at rest (AES-256-GCM in Convrail’s case), and delete on request.
Deletion requests, applied by hash
Shopify makes the deletion flow concrete with three mandatory webhooks described in its privacy law compliance documentation:
| Webhook | When Shopify sends it | What the app must do |
|---|---|---|
customers/data_request | When a customer requests their data from the store owner | Provide the customer’s data to the store owner, using the resource IDs in the payload |
customers/redact | “If a customer hasn’t placed an order in the past six months, then Shopify sends the payload 10 days after the deletion request. Otherwise, the request is withheld until six months have passed” | Redact or delete the customer’s data |
shop/redact | “48 hours after a store owner uninstalls your app” | “erase data for that store from your database” |
For each, the app must “complete the action within 30 days of receiving the request” and acknowledge with a 200-series status.
The catch for a connector that never stores clear values: the customers/redact payload contains the customer’s email and ID in clear, and the connector holds only hashes. The answer is to hash the incoming identifiers with the exact same normalization used at capture time and delete every event whose emails_sha256 or external_ids_sha256 matches. This is one more reason the normalization must be deterministic and never change: a store that silently switched from trimming to not trimming emails would be unable to find, and therefore unable to delete, the records it created before the switch. Convrail applies deletion this way and purges the entire store on shop/redact.
How Convrail’s guard blocks clear personal data
Hashing rules are only as good as their enforcement. A refactor, a new field mapped from a webhook, a debugging log left on: any of these can put a clear email back into a payload months after the original code was reviewed. Convrail puts a guard between the batch builder and the HTTP client, and it runs on every outgoing payload without exception.
The guard refuses to send a batch when any of the following is found anywhere in the serialized payload:
- a string matching an email pattern (a local part, an
@, a domain); - a forbidden key such as
email,phoneorfirst_name, regardless of where it appears in the structure; - a hashed field whose value is not exactly 64 lowercase hexadecimal characters.
A refused payload never reaches the network. The guard itself is covered by an automated test that tries to smuggle clear personal data through and must fail; if a future change weakens the guard, the test fails and the change does not ship.
This is the property to demand from any server-side tracking tool: not “we hash”, which is a statement about intent, but “clear personal data cannot leave, and here is the mechanism that enforces it”.
Common mistakes
- Hashing without normalizing. Uppercase letters, stray whitespace and a national phone format each produce a hash OpenAI cannot match. The hash is fine; the input was wrong.
- Hashing empty strings. A valid-looking digest of nothing, sent as a match key.
- Applying the leading-zero rule to national numbers.
06 12 34 56 78becomes612345678and never matches33612345678. Convert to E.164 first. - Transliterating accents in names. The rule removes ASCII punctuation only;
zoëstayszoë. - Salting. A salted hash cannot be matched by anyone who does not know the salt, which defeats the purpose. Advanced matching uses unsalted SHA-256 by design; the privacy protection comes from consent and minimization, not from the salt.
- Treating hashed data as out of GDPR scope. It is pseudonymised personal data. Consent, information and deletion obligations remain.
- Putting identifiers in URLs. The image tag reference forbids personal data and order identifiers in query parameters; the same goes for your own relay endpoints and logs.
- Changing the normalization later. Every past hash becomes unmatchable and, worse, undeletable.
What to do next
Check what your current tracking setup sends by opening one real payload and searching for @; if you would rather have a guard do that on every batch, the Convrail tracking module hashes on receipt, blocks clear personal data automatically and applies deletion webhooks by hash.
Sources
Frequently asked questions
Does hashing a customer email make it anonymous under GDPR?
No. A SHA-256 hash of an email is pseudonymised data: it can be linked back to the person by anyone who holds the email, so it stays personal data and the GDPR still applies, including the need for a lawful basis such as consent.
Which customer fields does the OpenAI Conversions API accept in clear text?
IP address, user agent, countries, regions, cities, postal codes, the obref browser reference and the Android advertising ID. Emails, phone numbers, external IDs, first names and last names are accepted only as SHA-256 hashes.
How do I normalize a phone number before hashing it for the Conversions API?
Remove the leading plus sign, leading zeros, whitespace, parentheses, periods and hyphens, and keep 8 to 15 digits. Store numbers in international format first so that a national number and its international form hash to the same value.
Who is the data controller when a tracking connector sends hashed data to OpenAI?
The merchant. The store decides why and how customer data is processed for advertising measurement; a connector such as Convrail processes it on the merchant's behalf under a contract, which is the processor role defined in GDPR Article 4(8).
What happens to hashed data when a customer asks to be deleted?
The deletion request is applied by hashed identifier. Convrail purges every event matching the customer's hashed identifiers when the Shopify customers/redact webhook arrives, and a shop/redact webhook purges the whole store.