Intempt Docs
GuidesPrivacy

Masking and hashing

Three places to control a sensitive field in Intempt: block, mask or hash it as it arrives, mask it in the console, or hash it in your own code before you send it.

Overview

A field that holds personal data has three points where you can do something about it, and they solve different problems.

WhereWhat it doesWho it protects against
As it arrivesThe raw value never enters your projectEveryone, including Intempt
In the consoleThe value is stored but shown maskedMost of your team
In your own codeYou send a hash instead of the valueEveryone, including Intempt

Pick by what you're actually worried about. If a field is genuinely never needed, the cheapest control is not sending it at all.

How it works

Rules at ingestion

Declare what should happen to a field before it lands. Rules run as the record arrives, so the raw value is never written.

ActionResult
BlockThe field is dropped. Nothing is stored
MaskThe value is replaced with a masked form. dana@northbay.io becomes d•••@northbay.io
HashThe value is replaced with a hash. Rows stay countable and joinable, the original is gone

A rule applies to a named field across every source in the project, so you don't have to remember to apply it per integration.

📘 Good to know

This is the strongest of the three, because there's no raw value anywhere to leak, export, or have to delete later. It's also the only one that helps with data already flowing from a source you don't control.

Masked display in the console

Sometimes the value has to be stored, because support needs it occasionally. Masking in the console keeps it out of everyone else's view.

Every attribute has a visibility state:

StateWho sees the value
VisibleAnyone with access to the project
HiddenNobody sees it in the console

Today that choice is all-or-nothing, and the real need rarely is. A support agent confirming a customer's email needs the last part of it. An analyst building a funnel does not. A Masked state between the two, showing a partial value to most roles and the full value to roles you name, is planned but not yet available.

Pair it with a custom role so the exception is explicit and auditable. See Access management.

Hashing in your own code

If Intempt should never see a value at all, hash it before you send it.

Two different reasons to hash, and they need different algorithms. Getting this wrong is the most common mistake in this area.

Your goalUseWhy
Intempt can't read the valueHMAC-SHA256 with a key only you holdWithout the key, the value can't be recovered or guessed
A destination can match the userNormalized SHA-256The destination recomputes the same hash and matches on it

Hashing for protection

import { createHmac } from 'node:crypto';

const pseudonym = createHmac('sha256', process.env.INTEMPT_HASH_KEY)
  .update(user.email.trim().toLowerCase())
  .digest('hex');

intempt.identify({ userId: pseudonym });

Keep the key on your server. Rotate it and every identifier changes, so treat it as permanent once you're live.

Hashing for destination matching

Ad platforms recompute the hash on their side, so the input has to be normalized exactly the way they expect: lowercase, trim whitespace, then SHA-256. Some destinations add rules of their own, such as stripping periods and +suffix from Gmail addresses.

import { createHash } from 'node:crypto';

const matchKey = createHash('sha256')
  .update(user.email.trim().toLowerCase())
  .digest('hex');

📘 Good to know

These two are not interchangeable. An HMAC will never match what a destination computes, and the integration doesn't error, it just returns nothing. If matching stops working after a privacy change, check which hash is being sent.

Where hashing can and can't happen

Client SDKs run on your users' devices, so anything in the bundle is readable, including a key. That makes a secret impossible to keep there.

Client SDKs (JavaScript, iOS, Android, React Native)Server SDKs (Node, Python, PHP)
KeyPublic write key onlySecret key, held by you
HashingNot available. Hash before the value reaches the clientAvailable

A hash computed in a browser protects nothing, because the raw value was already in the browser.

What none of this makes anonymous

Hashing and masking are pseudonymization, not anonymization. A pseudonymized value is still personal data under GDPR, still needs a lawful basis, and still has to be included when a user asks for their data or asks to be deleted.

A plain SHA-256 of an email is also reversible in practice. Email addresses are predictable enough that an attacker can hash a candidate list and match it. That's why protection needs a key and matching doesn't count as protection.

The only technique here that removes personal data is blocking the field at ingestion.

Choosing

If you...Do this
Never need the fieldStop sending it
Can't stop a source sending itBlock it at ingestion
Need to count or segment on it, never read itHash it at ingestion, or HMAC it in your code
Need support to read it occasionallyStore it, keep it Visible, and restrict the project to the roles that need it
Need a destination to match on itNormalized SHA-256, in a server SDK
Are told encryption is the answerIt isn't. Encrypted values can't be grouped or joined, so the field stops being usable

Use cases

  1. A source you don't control sends a national ID field. Block it at ingestion so it never lands.
  2. Support needs to confirm a customer's email but analysts don't. Today this needs separate role grants; a Masked state is planned.
  3. You want to segment by customer without Intempt holding anything readable. HMAC the customer number in your backend and send the result.
  4. Your ad destination stopped matching after a privacy change. Check whether an HMAC is being sent where a normalized SHA-256 is expected.
  5. A form field started collecting free text that sometimes contains names. Hide the attribute while you fix the form.
  6. An auditor asks which fields hold personal data and who can read them. Filter attributes by state and export the role grants.
  7. A developer asks whether to hash in the browser. Point them here: the raw value is already in the browser, so it buys nothing.

Where to go next

On this page