pii · Guide
Redaction & tokens
Four actions share one detection pipeline: report-only (detect), remove-and-tag (redact), partial-mask (mask), and reversible substitution with a vault (tokenizeText/detokenizeText).
The four actions
| Function | outputText | Use it for |
|---|---|---|
detect | Unchanged — identical to the input | Report findings only, without touching the text. |
redact | Every finding replaced with [REDACTED_<CATEGORY>] | Logging or storage where the shape of the removed data does not need to be recoverable at all. |
mask | Every finding replaced with a partial mask — see below | Human-readable output that still hints at the original (j***@gmail.com, a card ending 1111). |
tokenizeText | Every finding replaced with a [PREFIX_N] token, with a vault mapping tokens back to real values | Reversible redaction: pass the same output text and vault to detokenizeText to restore the original. |
Every function has an Async counterpart (detectAsync, redactAsync, maskAsync, tokenizeAsync) that is the only way to reach the dictionary and NER tiers — the synchronous versions always run the pattern tier only, regardless of detection.mode.
Mask strategies
Each category's mask field picks one of four strategies, applied when the action is mask (or detect/tokenize, which use the same partial mask for display; redact always uses the fixed [REDACTED_<CATEGORY>] tag regardless of the category's own mask kind):
| Kind | Behaviour | Example categories |
|---|---|---|
fixed | A constant replacement string. | birthDate → [DOB], password → [PASSWORD], person → [PERSON] |
keep-edges | Keeps head characters at the start and tail at the end, fills the middle with *. | iban (4 head / 4 tail), apiKey (4/4), tr_plaka (2/0) |
keep-last | Keeps the last tail characters, fills the rest with *. | phone/tr_phone/de_phone (4), creditCard (4), tc_kimlik/tr_vkn (4) |
keep-domain | Email-specific: keeps the domain, masks the local part except its first character. | email (ahmet@example.com → a****@example.com) |
A value shorter than the characters a strategy would keep is masked entirely (fill.repeat(value.length)), so a strategy never accidentally reveals more than it hides on a short match.
The finding shape
Every entry in findings (JS) / "findings" (Python) carries:
| Field | Type | Meaning |
|---|---|---|
category | string | The category id (email, tc_kimlik, …). |
value | string | The exact matched substring. |
start / end | number | Character offsets into the input — UTF-16 code units in JS, Unicode code points in Python. See the offset note below. |
label | string | Human-readable category label, localised via locale. |
message | string | A formatted sentence, e.g. "Email address detected". |
severity | 'low' | 'medium' | 'high' | From the category definition. |
confidence | number | The fused score after context boosting (and, in pattern+ner-verified, judge fusion). |
action | string | The action that produced this finding ('detect', 'redact', 'mask', 'tokenize'). |
block | boolean | true only when action === 'block'. |
replacement | string | What this finding was replaced with in outputText (or would be). |
detector | 'pattern' | 'dictionary' | Which tier raised the candidate. |
evidence | string[] | Human-readable reasons — 'checksum/format validated', 'context word "tel"', 'spelled digits', and so on. |
The top-level result also carries inputLength, outputText, hasBlocking (true if any finding has block: true), action, languages, and — only when non-empty — degraded (a list of reasons the NER tier fell back, such as a timeout or a missing runtime).
Overlapping findings
When two candidates overlap, the higher-severity one wins; ties are broken by the longer span. Findings are otherwise ordered by their start offset, longer-first among equal starts, before overlap resolution runs — so a phone finding losing to a birthDate finding on the same digits (a dotted date that also looked phone-shaped) reflects an actual measured false-positive fix, not an arbitrary priority order. See Categories for the validator-level guards (validatePhone's date and IP exclusions, for example) that prevent most of these collisions before overlap resolution is even needed.
Reversible tokenization
const { outputText, vault } = pii.tokenizeText('Kart no: 4111111111111111');
// outputText: 'Kart no: [CREDITCARD_1]'
// vault: { '[CREDITCARD_1]': { value: '4111111111111111', category: 'creditCard' } }
pii.detokenizeText(outputText, vault); // { outputText: 'Kart no: 4111111111111111' }The same (category, value) pair always maps to the same token within one call, so a value repeated in the input is not assigned two different tokens. Token prefixes are derived from the category id (uppercased, non-alphanumerics collapsed to _), numbered per prefix starting at 1. detokenizeText replaces the longest tokens first, so one token string that happens to be a prefix of another never gets partially substituted.
The vault is caller-managed
tokenizeText does not persist the vault anywhere — you own storing it (a database row, a session, an encrypted blob) and matching it back up with the tokenized text later. Losing the vault means the original values are unrecoverable from the tokenized text alone.
Where to go next
- Categories — every category's mask strategy and default state.
- Detection modes — how
confidenceis computed in each mode. - JavaScript API and Python API — exact function signatures and return shapes.

