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.
| Where | What it does | Who it protects against |
|---|---|---|
| As it arrives | The raw value never enters your project | Everyone, including Intempt |
| In the console | The value is stored but shown masked | Most of your team |
| In your own code | You send a hash instead of the value | Everyone, 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.
| Action | Result |
|---|---|
| Block | The field is dropped. Nothing is stored |
| Mask | The value is replaced with a masked form. dana@northbay.io becomes d•••@northbay.io |
| Hash | The 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:
| State | Who sees the value |
|---|---|
| Visible | Anyone with access to the project |
| Hidden | Nobody 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 goal | Use | Why |
|---|---|---|
| Intempt can't read the value | HMAC-SHA256 with a key only you hold | Without the key, the value can't be recovered or guessed |
| A destination can match the user | Normalized SHA-256 | The 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) | |
|---|---|---|
| Key | Public write key only | Secret key, held by you |
| Hashing | Not available. Hash before the value reaches the client | Available |
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 field | Stop sending it |
| Can't stop a source sending it | Block it at ingestion |
| Need to count or segment on it, never read it | Hash it at ingestion, or HMAC it in your code |
| Need support to read it occasionally | Store it, keep it Visible, and restrict the project to the roles that need it |
| Need a destination to match on it | Normalized SHA-256, in a server SDK |
| Are told encryption is the answer | It isn't. Encrypted values can't be grouped or joined, so the field stops being usable |
Use cases
- A source you don't control sends a national ID field. Block it at ingestion so it never lands.
- Support needs to confirm a customer's email but analysts don't. Today this needs separate role grants; a Masked state is planned.
- You want to segment by customer without Intempt holding anything readable. HMAC the customer number in your backend and send the result.
- Your ad destination stopped matching after a privacy change. Check whether an HMAC is being sent where a normalized SHA-256 is expected.
- A form field started collecting free text that sometimes contains names. Hide the attribute while you fix the form.
- An auditor asks which fields hold personal data and who can read them. Filter attributes by state and export the role grants.
- 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
Data classification and views
Label which fields hold personal data, and give each team a view of your project containing only what they need.
Consent Management Overview
Consent management tracks what each customer agreed to and controls who your messages reach. It's real, and it's distributed by design across the Users tab and the Journey builder, not one settings screen.
