Guide

How to Send Hashed User Data with the OpenAI Ads Pixel

Supplying hashed identifiers — most commonly a customer's email address — to the OpenAI Ads pixel and Conversions API improves how conversion events match back to the ad clicks that produced them. Identifiers are converted to SHA-256 hashes after normalization (trimmed and lowercased), and raw personal information should never go anywhere a hashed field is specified. The pixel can perform this hash for you, or you can hash before sending; either way, OpenAI's servers only ever receive the hash.

Why match quality matters

Every conversion event you send to OpenAI Ads — a purchase, a lead form submission, a completed registration — needs to connect back to the ad click that led to it. When an event carries no identity signal beyond the click reference itself, it has fewer ways to match to the right click, especially across sessions or devices: someone clicks an ad on their phone, closes the tab, and completes the purchase later on a laptop.

The oppref value the OpenAI Ads pixel captures automatically into a first-party __oppref cookie handles the same-device, same-session case well. It's a privacy-preserving click reference with roughly a 30-day window, and for a lot of conversions, it's sufficient on its own. But it's tied to a browser context, not a person. A hashed identifier like email_sha256 gives OpenAI a second signal that isn't tied to a single device or cookie lifespan — it's the same hash whether the customer converts on their phone or their laptop, as long as they use the same email both times.

That's the practical reason "send hashed user data" is a recommendation rather than an edge-case feature: it's the difference between an attribution system that only sees within-session behavior and one that can close the loop when a customer's path to conversion spans devices.

Normalization: the rule that has to match everywhere

SHA-256 is deterministic. The same input string always produces the same 64-character hexadecimal hash, and different input strings — even by a single character of casing or whitespace — produce completely different hashes. If you hash Jane@Example.com on one side and jane@example.com on the other, you get two different hashes for what is, to everyone except SHA-256, the same email address.

OpenAI's documented normalization rule for email_sha256 is straightforward: trim whitespace, convert to lowercase, then hash. Apply that exact sequence, in that exact order, every time you produce this hash — whether it's happening in browser-side JavaScript before an oaiq("init", ...) call or in server-side code before a Conversions API request.

The failure mode worth naming: if your pixel-side code normalizes correctly but your CAPI-side code (built later, by a different person, copied from a different reference) skips the lowercase step, the two hashes for the same customer's email will never match each other. Nothing in the API will error — you'll just have two silently uncorrelated hashes instead of one consistent identifier. This is the single most common way hashed user data implementations quietly fail.

Normalizing and hashing an email in JavaScript

async function hashEmail(rawEmail) {
  const normalized = rawEmail.trim().toLowerCase();
  const encoder = new TextEncoder();
  const data = encoder.encode(normalized);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}

// Usage
const emailSha256 = await hashEmail("  Jane@Example.com  ");
// -> same 64-character hex string every time, regardless of
//    the casing or surrounding whitespace in the original input

crypto.subtle.digest is the Web Crypto API's built-in SHA-256 implementation, available in modern browsers without a third-party library. The important part isn't the hashing call — it's the trim().toLowerCase() step before it. Skip that, and the hash is technically valid SHA-256 output but practically useless for matching, because it won't agree with a hash of the same email produced wherever normalization was applied correctly.

Pixel-side: supplying user data at init

The OpenAI Ads pixel's user object is added to oaiq("init", ...), not to individual oaiq("measure", ...) calls. Every field is optional — include only what you actually have for the current visitor.

<script>
  oaiq("init", {
    pixelId: "<YOUR-PIXEL-ID>",
    user: {
      email_sha256: "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514",
      external_id_sha256: "73d83a078369bb4f0971b317aa7797a91cf5c0df1b62161c2e47d75c33ab5b6e",
      country: "US",
      city: "San Francisco",
      zip_code: "94107"
    }
  });
</script>

A few specifics worth noting:

  • User data is request-scoped at init, not per-event. Set it once when you call init — don't repeat it on every subsequent measure call.
  • If user data becomes available later — most commonly, a visitor logging in after the pixel already initialized — call init again with the complete user object. You can omit pixelId on that second call.
  • external_id_sha256 is a hash of a stable, pseudonymous identifier from your own system (an internal customer ID, for example) — not a second copy of the email.
  • city is normalized the same way as email: OpenAI trims whitespace and lowercases it server-side. country should be a two-letter ISO 3166-1 code like US; zip_code allows letters, numbers, spaces, or hyphens up to 32 characters.
  • Send hashes as lowercase, 64-character hexadecimal strings. Don't send raw email addresses, raw external IDs, phone numbers, or phone number hashes; there's no documented field for any of those.

CAPI-side: hashing server-side with the same normalization

On the Conversions API, the user object lives inside each individual event rather than at a request root — it's event-scoped, matching a single conversion rather than a whole session.

{
  "id": "order_12345",
  "type": "order_created",
  "timestamp_ms": 1773892800000,
  "action_source": "web",
  "source_url": "https://shop.example.com/checkout/confirmation",
  "user": {
    "email_sha256": "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514",
    "external_id_sha256": "73d83a078369bb4f0971b317aa7797a91cf5c0df1b62161c2e47d75c33ab5b6e",
    "country": "US",
    "city": "San Francisco",
    "zip_code": "94107",
    "ip_address": "203.0.113.1",
    "user_agent": "Mozilla/5.0"
  },
  "data": {
    "type": "contents"
  }
}

This request is sent from your server only, using POST https://bzr.openai.com/v1/events?pid=<PIXEL-ID> with an Authorization: Bearer <API-KEY> header. The CAPI-side user object accepts two fields the pixel doesn't — ip_address and user_agent — which makes sense given CAPI runs server-side, where request metadata like the visitor's IP is naturally available in a way it isn't client-side.

The part that has to be identical to the pixel side is the hashing logic for email_sha256 and external_id_sha256: trim, lowercase, then SHA-256. If your backend uses a different language than your frontend — Python or Ruby on the server, JavaScript in the browser — the hashing library will differ, but the normalization steps before hashing need to produce byte-for-byte the same input string, or the two hashes won't correlate even though both are technically valid SHA-256 output.

For a full CAPI request/response walkthrough, see our Conversions API setup guide; for the server-only boundary this endpoint requires, see secure CAPI implementation.

What not to send

Hashed user data is a narrow, specific feature — five documented fields on the pixel, seven on CAPI. It's not a general-purpose channel for passing customer data to OpenAI.

  • Never put raw PII in a field documented as hashed. email_sha256 means a SHA-256 hash goes there, not a plain-text email "OpenAI will presumably hash on its end." OpenAI's documentation is explicit that the hash happens before the value reaches this field — a raw email in email_sha256 isn't a shortcut, it's the wrong kind of value in a field with a specific expected format, and it defeats the point of the field being hashed at all.
  • Don't over-collect into custom fields. There's no documented mechanism for adding arbitrary customer data — order history, purchase preferences, loyalty tier — into the pixel or CAPI user object. If a field isn't on OpenAI's documented list (email_sha256, external_id_sha256, country, city, zip_code, and on CAPI, ip_address and user_agent), it doesn't belong there.
  • Apply data minimization. Send only the fields you actually have and that genuinely improve matching. If you don't have a customer's zip code, don't backfill it from an unrelated source just to populate the field — an incomplete but accurate user object beats a fuller one built from mismatched data.

The underlying principle is the one that governs good data practice generally: collect and transmit only what a specific, documented purpose requires, nothing more.

Hashing an identifier doesn't remove the need for consent to process it. A SHA-256 hash of an email is still derived from personal data belonging to an identifiable person, and most consent frameworks — and increasingly, most consent management platforms — treat sending a hash the same way they'd treat sending the underlying value: it requires the same lawful basis or opt-in the raw data would.

In practice, this means your consent tool needs to gate whether the user object gets populated at all, not just whether the pixel or CAPI event fires. A visitor who has consented to analytics tracking but not to more granular personalization might be a case where you still fire a standard event like page_viewed or order_created, but withhold email_sha256 and external_id_sha256 from the user object on those events. The event still contributes to aggregate measurement; identity-level matching only happens where consent explicitly allows it.

Concretely, this usually looks like a conditional check before your init call or CAPI request assembly: read the current consent state from whatever tool manages it on your site, and only populate the user object's fields when that state permits identity-level data sharing. Firing everything by default and hoping a downstream system filters it out afterward is the wrong order of operations; the gate needs to sit in front of the data leaving your systems, not after.

Verifying hashes match across pixel and CAPI

Because SHA-256 is deterministic, verification is mechanical — a matching hash is either produced by identical normalized input, or it isn't a match at all.

  1. Pick a known test value. Use a test email you control, run it through both your pixel-side hashing code and your server-side CAPI hashing code, and compare the two resulting hex strings directly.
  2. Check the pixel's debug output. Initialize the pixel with debug: true in a test environment and inspect the browser console — the SDK logs its activity, confirming what's actually sent in the user object rather than what you assume your code produces.
  3. Inspect the CAPI request body in a staging environment (not by logging real customer payloads in production — see the data-minimization note above) and compare email_sha256 against what the pixel produced for the same test input.
  4. If they don't match, check casing and whitespace first. A stray .trim() that only runs on one side, or a form field that adds a trailing space the pixel captures but your backend strips differently, accounts for most mismatches.
  5. Re-verify after any refactor that touches either the frontend or backend hashing code — a normalization mismatch from an unrelated change is easy to miss without an explicit check.

A hash mismatch doesn't throw an error anywhere in the pipeline. Both events send successfully, both look structurally valid, and the only symptom is quietly worse cross-device matching than you'd get if the hashes agreed — which is why this is worth checking deliberately rather than assuming it works because nothing broke.


Getting hashed user data right is less about the hashing itself — SHA-256 is a solved problem — and more about keeping normalization identical everywhere it happens, and keeping consent in front of the data rather than as an afterthought. Both are easy to get right when built in from the start of a pixel and CAPI implementation, and easy to drift apart when the pixel side and the server side are built at different times by different people.

Want this done for you?

Fixed-scope setup, tested end-to-end and documented.

Explore the serviceTake the free audit