Libraries can return a SHA-256 digest as raw 32 bytes, lowercase hexadecimal, uppercase hexadecimal, Base64, or a buffer object. An integration may then serialize, escape, or hash that value again. Two systems can both say SHA-256 while sending different strings. The result is low match quality, rejected user identifiers, or an apparent upload success that never joins to the expected lead.
Use this when browser, server, CRM, Data Manager, or Google Ads API enhanced-conversion hashes disagree despite using the same normalized email or phone value.
Quick answer
Choose one non-personal test value and write down its exact normalized UTF-8 bytes. Produce the raw 32-byte digest, lowercase hexadecimal form, uppercase hexadecimal form, Base64 form, and a deliberately double-hashed form. Record string length and byte length at each boundary. Compare the representation required by the current Google endpoint with the final JSON payload, not the library object before serialization. Keep only a short-lived redacted fixture in logs and never log a real user's raw identifier beside its hash.
Test scenarios to run
Run the same controlled fixture across these branches. Write down the expected result before testing so a surprising response is easy to identify.
| Scenario | Fixture | Expected result |
| Lowercase hex | 64 ASCII characters | Use when endpoint requires hex |
| Uppercase hex | 64 ASCII characters | Normalize representation |
| Base64 | 44 characters usually | Reject for hex field |
| Double hash | Hash of digest string | Remove second pass |
Diagnostic table
Use this table to connect the observed behavior to evidence and a verification step.
| Action | Evidence | Verification |
| Freeze one safe byte fixture | Document normalization, character encoding, digest algorithm, output representation, JSON field, endpoint, library, and runtime version. | The fixture has the same normalized bytes in every runtime. |
| Enumerate digest representations | Use a fixed non-personal fixture to compare browser, PHP, CRM, queue worker, Data Manager, and Google Ads API implementations. | The endpoint receives the required lowercase hexadecimal representation. |
| Inspect the final payload | Capture lowercase hex length, Base64 length, raw buffer length, casing, prefix, whitespace, quoting, escaping, and accidental byte-array serialization. | No client, middleware, queue, or connector hashes the digest twice. |
| Remove double hashing | Search for a second hashing step in tag managers, middleware, CRM connectors, queue consumers, and upload helpers. | Upload diagnostics, consent, identifiers, timing, and downstream match evidence reconcile. |
What to check first
- Document normalization, character encoding, digest algorithm, output representation, JSON field, endpoint, library, and runtime version.
- Use a fixed non-personal fixture to compare browser, PHP, CRM, queue worker, Data Manager, and Google Ads API implementations.
- Capture lowercase hex length, Base64 length, raw buffer length, casing, prefix, whitespace, quoting, escaping, and accidental byte-array serialization.
- Search for a second hashing step in tag managers, middleware, CRM connectors, queue consumers, and upload helpers.
- Verify consent, event time, lead identifiers, click identifiers, conversion action, partial failures, and diagnostics after the hash matches.
Field notes
- Write the expected result before changing anything and keep one repeatable synthetic fixture for the full test window.
- Record exact versions and UTC timestamps because caches, retries, scheduled actions, and deployments can change the evidence between checks.
- Test the public path and the stored server-side result, not only an admin preview, isolated command, or API response.
- Review the result again after the relevant cache, queue, cron, webhook, and observation window has completed.
Useful command or data shape
Adapt paths, IDs, and privacy handling to the site before running commands or storing data on production.
value='test@example.com'
printf '%s' "$value" | shasum -a 256
printf '%s' "$value" | openssl dgst -sha256 -binary | base64
# Compare the required endpoint representation with the final JSON field.
# Never use real customer data in shell history.
Why this usually happens
- A cryptography function defaults to Base64 while another defaults to hexadecimal.
- A raw byte buffer is coerced into a comma-separated array or object in JSON.
- A middleware hashes values already marked as normalized and hashed.
- Debug comparisons use different Unicode normalization or character encodings before SHA-256.
Decision rule
Declare parity only when normalized input bytes, SHA-256 digest, required string representation, and final serialized payload match across every participating system.
Production verification checklist
- The fixture has the same normalized bytes in every runtime.
- The endpoint receives the required lowercase hexadecimal representation.
- No client, middleware, queue, or connector hashes the digest twice.
- Upload diagnostics, consent, identifiers, timing, and downstream match evidence reconcile.
Safe fix order
Use a sequence that makes each result easy to prove. Stop when new evidence changes the scope or owner of the problem.
- Freeze one safe byte fixture
- Enumerate digest representations
- Inspect the final payload
- Remove double hashing
- Retest upload diagnostics
Mistakes to avoid
- Changing production before recording exact versions, UTC timestamps, a stable fixture, the expected result, and a tested rollback point.
- Treating one successful screen as proof while logs, stored records, background jobs, caches, emails, APIs, and downstream systems remain unchecked.
- Testing only as an administrator instead of using the role, device, locale, cache state, request path, and failure branch that users actually reach.
- Leaving debug output, temporary exclusions, helper accounts, duplicate hooks, broad permissions, or relaxed firewall rules active after verification.
Questions teams ask during testing
Can I test this directly in production?
Start with read-only evidence. Use staging for code, package, security, checkout, form, privacy, or cache changes. If a production canary is necessary, make it identifiable, reversible, monitored, and incapable of exposing personal data or charging a customer.
How do I avoid a false positive?
Repeat the same fixture with the same versions, URL, role, locale, cache state, and downstream integration. Compare the public result, stored result, and logs instead of relying on one browser view.
What evidence should I retain?
Keep UTC time, exact versions, request or record ID, expected result, actual result, relevant log lines, change made, rollback point, owner, and final verification. Redact credentials, tokens, and personal data.
When is the work complete?
Close it when the primary path passes, failure branches are understood, stored and downstream records reconcile, temporary changes are removed, monitoring is active, and the owner has the evidence packet.
What to tell the client or owner
Give the owner a concise packet with the affected workflow, exact versions, UTC test time, synthetic fixture ID, expected result, actual result, key logs, change made, rollback point, final result, unresolved risks, owner, and next review date. Remove credentials and personal data before sharing it.
When HandL WP should help
Bring in help when this affects leads, checkout, search visibility, security, paid media reporting, or a client production site. HandL WP can trace the issue through WordPress, hosting, cache, tracking, and Search Console, then verify the workflow after the technical fix.
If this is active on a production site, have HandL WP trace an enhanced-conversions mismatch.
Related HandL WP guides
Use these related guides when the same issue touches tracking, security, checkout, or crawler visibility.
Helpful references