<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michele</title>
    <description>The latest articles on DEV Community by Michele (@mmichele1).</description>
    <link>https://dev.to/mmichele1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3962183%2Fe99816db-d7c7-4875-ab31-6bcb00e20504.jpg</url>
      <title>DEV Community: Michele</title>
      <link>https://dev.to/mmichele1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mmichele1"/>
    <language>en</language>
    <item>
      <title>Choosing a PII anonymization strategy: when mask, redact, replace, hash, or substitute wins</title>
      <dc:creator>Michele</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/mmichele1/choosing-a-pii-anonymization-strategy-when-mask-redact-replace-hash-or-substitute-wins-54e9</link>
      <guid>https://dev.to/mmichele1/choosing-a-pii-anonymization-strategy-when-mask-redact-replace-hash-or-substitute-wins-54e9</guid>
      <description>&lt;p&gt;Any tool that anonymizes PII from free text gives you roughly five levers:&lt;br&gt;
&lt;strong&gt;mask&lt;/strong&gt;, &lt;strong&gt;redact&lt;/strong&gt;, &lt;strong&gt;replace&lt;/strong&gt;, &lt;strong&gt;hash&lt;/strong&gt;, and &lt;strong&gt;substitute&lt;/strong&gt;. They all&lt;br&gt;
answer one question — how much of the original value do you keep, and in what&lt;br&gt;
form? The choice is rarely global. A support screen needs the last four of a&lt;br&gt;
card to be recognisable; a log aggregator needs a stable join key that is not&lt;br&gt;
the original email; a screenshot fixture for a compliance deck needs one&lt;br&gt;
canonical placeholder, not a different synthetic value every run. Pick the&lt;br&gt;
strategy per field, not per codebase. This article walks through each&lt;br&gt;
strategy, what it does to the value, and a decision matrix for picking the&lt;br&gt;
right one per use case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxyep5g7shxq11ok5svoo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxyep5g7shxq11ok5svoo.png" alt="Five PII anonymization strategies compared: mask, redact, replace, hash, substitute. The same input email, jane.doe@example.com, transformed by each strategy." width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The diagram shows the same input — the email &lt;code&gt;jane.doe@example.com&lt;/code&gt; — fed&lt;br&gt;
through each of the five strategies. The rest of this article explains why&lt;br&gt;
you would pick one over another and how to combine them in a single call.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the choice matters
&lt;/h2&gt;

&lt;p&gt;The strategy you pick is the privacy/utility knob for that field. Strip&lt;br&gt;
everything and you lose the diagnostic value of a log line; preserve&lt;br&gt;
everything and you have not anonymized anything. A default that is right for&lt;br&gt;
one team is rarely right for every team. A production log aggregator, a&lt;br&gt;
development seed dataset, and a screenshot fixture for a compliance training&lt;br&gt;
deck want three different things from the same email address. The rest of&lt;br&gt;
this article is about recognising which case you are in and choosing the&lt;br&gt;
strategy to match.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mask
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mask&lt;/strong&gt; keeps the structure of the value and hides part of it. You usually&lt;br&gt;
pick it when the value still has to look like itself, just less complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;anonymize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;EMAIL_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;char&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fromEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common default for &lt;strong&gt;credit cards&lt;/strong&gt; is to keep the last four digits,&lt;br&gt;
for &lt;strong&gt;IP addresses&lt;/strong&gt; to keep the first three octets, and for &lt;strong&gt;MAC&lt;br&gt;
addresses&lt;/strong&gt; to keep the OUI while hiding the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; support screens where an agent still needs the last four of a&lt;br&gt;
card or a recognisable email shape to find a record; logs that must stay&lt;br&gt;
greppable; and any UI that benefits from the user seeing a value is &lt;em&gt;there&lt;/em&gt;&lt;br&gt;
without showing the whole value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational note:&lt;/strong&gt; residual plaintext is still sensitive. Masking is a&lt;br&gt;
usability tool, not a privacy boundary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Redact
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Redact&lt;/strong&gt; is the simplest strategy: the detected value is removed entirely.&lt;br&gt;
The configuration object is just &lt;code&gt;{ type: "redact" }&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;anonymize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice Smith can be reached at asmith@mygoogle.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;EMAIL_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → "Alice Smith can be reached at "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a common default for &lt;strong&gt;URLs&lt;/strong&gt; when you want to keep the base address&lt;br&gt;
but drop query strings and other tracking material.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; free-text fields where the PII carries no diagnostic value —&lt;br&gt;
error messages, exception payloads, customer-support copy pasted into a&lt;br&gt;
ticket. It is the minimum-friction option when you genuinely do not need the&lt;br&gt;
value downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational note:&lt;/strong&gt; redact destroys all signal. If a downstream parser expects the&lt;br&gt;
entity to be a non-empty string, redact will hand it an empty slot and may&lt;br&gt;
crash on whitespace. Wrap it with a sentinel if your pipeline needs one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Replace
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Replace&lt;/strong&gt; substitutes every occurrence of the detected entity with one&lt;br&gt;
fixed value that you provide.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;anonymize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;EMAIL_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;replace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dummy@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace is rarely the default for any entity. The value you supply is&lt;br&gt;
global: every email in the request becomes &lt;code&gt;dummy@example.com&lt;/code&gt;, every phone&lt;br&gt;
number becomes &lt;code&gt;+1-555-0100&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; seed data for screenshots and compliance training decks;&lt;br&gt;
demo fixtures that have to look like real data but must not be real;&lt;br&gt;
synthetic test inputs where you want a single canonical placeholder rather&lt;br&gt;
than a different synthetic value per occurrence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational note:&lt;/strong&gt; every detected entity collapses to one value. If your dataset&lt;br&gt;
has a hundred email addresses, they all become &lt;code&gt;dummy@example.com&lt;/code&gt; — so&lt;br&gt;
uniqueness is destroyed by construction, and any downstream check that&lt;br&gt;
relies on distinctness (duplicate detection, per-user correlation, group-by)&lt;br&gt;
will produce nonsense. Replace is for &lt;em&gt;display&lt;/em&gt;, not analysis.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hash
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hash&lt;/strong&gt; turns the detected value into a fixed-length token. Most&lt;br&gt;
implementations use SHA-256 and, when a salt is provided, produce the same&lt;br&gt;
token for the same input across calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;anonymize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0123456789abcdef&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;IP_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hash is the common choice for &lt;strong&gt;crypto addresses&lt;/strong&gt; and, in practice, a very&lt;br&gt;
common choice for &lt;strong&gt;IP addresses&lt;/strong&gt; in log sanitisation. Without a salt, each&lt;br&gt;
request hashes independently and the same value can produce a different token&lt;br&gt;
on every call; with a salt, the output is deterministic across calls and&lt;br&gt;
across services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; cross-record correlation. If you need to trace one user&lt;br&gt;
across dozens of log lines without storing the original value, hash with a&lt;br&gt;
stable salt gives you the join key. The token is a stable handle for the&lt;br&gt;
record — every log line that mentions the same email produces the same&lt;br&gt;
token, so you can join records across services without ever seeing the&lt;br&gt;
original.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational note:&lt;/strong&gt; hash is a pseudonymisation technique. The salt is a&lt;br&gt;
separate secret you must govern alongside the data — rotate it like any&lt;br&gt;
other credential, restrict who can read it, and never log it next to the&lt;br&gt;
hashes. If the link to the original is no longer needed, delete or rotate&lt;br&gt;
the salt so the records can no longer be re-derived.&lt;/p&gt;
&lt;h2&gt;
  
  
  Substitute
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Substitute&lt;/strong&gt; swaps the detected value for realistic synthetic data of the&lt;br&gt;
same type. Configuration is &lt;code&gt;type&lt;/code&gt; and an optional &lt;code&gt;attribute&lt;/code&gt; that hints&lt;br&gt;
what flavour of synthetic data to produce.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;anonymize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0123456789abcdef&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;EMAIL_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;substitute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a salt is provided, the same input always produces the same synthetic&lt;br&gt;
output across calls — every endpoint, every region, every CI run. With the&lt;br&gt;
salt, substitute becomes a join key: the same &lt;code&gt;jane.doe@example.com&lt;/code&gt; will&lt;br&gt;
appear in every record that originally referenced that user, without&lt;br&gt;
revealing anything about the original address. Without the salt, synthetic&lt;br&gt;
values vary by request, so substitute is only useful when you don't need to&lt;br&gt;
re-identify the same record later.&lt;/p&gt;

&lt;p&gt;Substitute is the common default for &lt;strong&gt;person names&lt;/strong&gt; (a fake name from the&lt;br&gt;
same locale), &lt;strong&gt;locations&lt;/strong&gt; (a synthetic address or state), &lt;strong&gt;phone&lt;br&gt;
numbers&lt;/strong&gt; (a fake number in the same format), and &lt;strong&gt;email addresses&lt;/strong&gt; (a&lt;br&gt;
fake address with a plausible domain). It is the closest strategy to a&lt;br&gt;
"functional but not real" copy of the original.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; analytics pipelines that still need to group by user; dev&lt;br&gt;
and staging environments seeded with realistic-looking data; datasets&lt;br&gt;
shared with third parties where the receiving system has to keep working&lt;br&gt;
but the subjects have to stay anonymous. Substitute preserves the&lt;br&gt;
&lt;em&gt;shape&lt;/em&gt; of the data, so downstream code, regexes, and validators all&lt;br&gt;
behave as they would with real values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational note:&lt;/strong&gt; synthetic is not original. Substitute does not preserve&lt;br&gt;
uniqueness, does not preserve relationships between fields (a substituted&lt;br&gt;
name and a substituted email will not belong to the same fake person),&lt;br&gt;
and on low-cardinality entity types (countries, currencies, states) the&lt;br&gt;
synthetic value can collide with the real one in the same dataset. For&lt;br&gt;
correlating records, use hash. For preserving function, use substitute.&lt;/p&gt;
&lt;h2&gt;
  
  
  Decision matrix — what each strategy does
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Output shape&lt;/th&gt;
&lt;th&gt;Reversible to original?&lt;/th&gt;
&lt;th&gt;Format kept?&lt;/th&gt;
&lt;th&gt;Length kept?&lt;/th&gt;
&lt;th&gt;Joinable across records?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mask&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partially redacted original&lt;/td&gt;
&lt;td&gt;n/a (partial plaintext remains)&lt;/td&gt;
&lt;td&gt;Yes (character class preserved)&lt;/td&gt;
&lt;td&gt;Configurable (&lt;code&gt;char&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;fromEnd&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Weak (the unmasked tail varies)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Empty (entity removed)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One fixed caller value&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (you choose)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — every entity collapses to one value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fixed-length hex (SHA-256, salted if a salt is set)&lt;/td&gt;
&lt;td&gt;Pseudonymisation — the salt is a separate secret you govern&lt;/td&gt;
&lt;td&gt;No (hex)&lt;/td&gt;
&lt;td&gt;Fixed (64 hex chars for SHA-256)&lt;/td&gt;
&lt;td&gt;Yes, with a salt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;substitute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Realistic synthetic value of the same type&lt;/td&gt;
&lt;td&gt;No (synthetic, not derivable)&lt;/td&gt;
&lt;td&gt;Yes (same type and locale)&lt;/td&gt;
&lt;td&gt;Approximately&lt;/td&gt;
&lt;td&gt;With a salt: same input → same synthetic output. Without: no.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Decision matrix — pick by use case
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you need to…&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Show a human the last four of a card, or a recognisable email shape&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mask&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preserves the part of the value that humans use to recognise the record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove PII from a free-text log line and never need the value again&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowest-friction default; the entity disappears entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trace one user across dozens of log lines without storing the original&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hash&lt;/code&gt; with a salt&lt;/td&gt;
&lt;td&gt;Deterministic token gives you a join key without keeping the value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed a dev environment or share a dataset with a third party without re-identification risk&lt;/td&gt;
&lt;td&gt;&lt;code&gt;substitute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthetic data keeps the pipeline functional, but the subject is fictional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build a screenshot fixture for a compliance training deck&lt;/td&gt;
&lt;td&gt;&lt;code&gt;replace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One global placeholder is easier to read in a static image than a hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanitise web-hook payloads from a payment provider before they reach your APM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mask&lt;/code&gt; (card) + &lt;code&gt;redact&lt;/code&gt; (everything else)&lt;/td&gt;
&lt;td&gt;Card last-four stays useful for support; everything else is noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default a new service to "no PII in logs" without bespoke per-entity config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;redact&lt;/code&gt; for free-text + &lt;code&gt;mask&lt;/code&gt; for cards&lt;/td&gt;
&lt;td&gt;Two overrides cover most cases; expand from there as you find needs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Combining all five in one call
&lt;/h2&gt;

&lt;p&gt;A real integration rarely uses one strategy. Most APIs let you key overrides&lt;br&gt;
by entity type, so you can mix strategies per entity in a single call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;anonymize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User Jane Doe (jane.doe@example.com) called from 203.0.113.42 about order #4711&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en_US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0123456789abcdef&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;PERSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;substitute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;EMAIL_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;char&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fromEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;IP_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;PHONE_NUMBER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;replace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+1-555-0100&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single call replaces the person with a synthetic name, masks the local&lt;br&gt;
part of the email, hashes the IP, redacts the URL, and collapses any phone&lt;br&gt;
number to a fixed placeholder. The five strategies compose per entity&lt;br&gt;
because they live in the same overrides map — the call itself does not pick&lt;br&gt;
a global mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How does hash relate to GDPR?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hash is a &lt;em&gt;pseudonymisation&lt;/em&gt; technique — exactly the kind of risk-reduction&lt;br&gt;
measure GDPR encourages for systems that hold personal data. A salted hash&lt;br&gt;
gives you the best of both worlds: the log aggregator no longer holds the&lt;br&gt;
original value (a meaningful improvement over plaintext), and the salt is a&lt;br&gt;
separate secret you can govern independently of the data — rotate it, scope&lt;br&gt;
it, audit access. If your goal is to correlate records across services&lt;br&gt;
while keeping the original out of your logs, a salted hash is the right&lt;br&gt;
tool. If your goal is to &lt;em&gt;break the link to the original entirely&lt;/em&gt;&lt;br&gt;
(irreversible anonymisation in the strict GDPR sense), that is a different&lt;br&gt;
problem solved by deletion, aggregation, or noise injection, not by&lt;br&gt;
hashing. The same framework also supports redaction, replacement, and&lt;br&gt;
substitution when you want to remove the original outright.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I get the original back from &lt;code&gt;substitute&lt;/code&gt; or &lt;code&gt;replace&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. &lt;code&gt;substitute&lt;/code&gt; generates a synthetic value with no relationship to the&lt;br&gt;
original that can be inverted; &lt;code&gt;replace&lt;/code&gt; substitutes one fixed value you&lt;br&gt;
supplied. Both are irreversible by design. If you need reversibility, the&lt;br&gt;
anonymisation tool is the wrong choice — you want encryption, not&lt;br&gt;
anonymisation, and you should think carefully about who holds the key and&lt;br&gt;
under what lawful basis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;mask&lt;/code&gt; GDPR-compliant?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on how much of the value you keep. Masking the first eight of&lt;br&gt;
sixteen card digits still leaves eight digits, which is enough to&lt;br&gt;
re-identify the cardholder against a known-card database. Mask is a&lt;br&gt;
&lt;em&gt;usability&lt;/em&gt; tool, not a privacy boundary. For a privacy boundary you want&lt;br&gt;
redact, replace, substitute, or hash — in roughly that order of strength.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;substitute&lt;/code&gt; preserve uniqueness?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The synthetic name &lt;code&gt;Jane Doe&lt;/code&gt; may be produced for a hundred different&lt;br&gt;
original subjects, and a substituted email may collide with a real email&lt;br&gt;
elsewhere in the dataset. If you need a join key, use hash with a salt.&lt;br&gt;
If you need realistic shape, use substitute. If you need both, apply them to&lt;br&gt;
different entity types in the same overrides block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I combine strategies per entity in one call?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — that is the normal mode. The overrides map is keyed by entity type&lt;br&gt;
(&lt;code&gt;PERSON&lt;/code&gt;, &lt;code&gt;EMAIL_ADDRESS&lt;/code&gt;, &lt;code&gt;IP_ADDRESS&lt;/code&gt;, and so on), and each value is its&lt;br&gt;
own strategy object. You can mix &lt;code&gt;mask&lt;/code&gt;, &lt;code&gt;redact&lt;/code&gt;, &lt;code&gt;replace&lt;/code&gt;, &lt;code&gt;hash&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;substitute&lt;/code&gt; freely across entity types in the same call. The one rule is&lt;br&gt;
that one entity type gets one strategy per call; if you want to apply two&lt;br&gt;
transformations to the same entity, you run the call twice or chain a&lt;br&gt;
second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are mapping this to a real API
&lt;/h2&gt;

&lt;p&gt;Use the same decision matrix, then swap in your provider’s exact entity names&lt;br&gt;
and field names from the docs. The trade-offs stay the same even when the&lt;br&gt;
syntax changes.&lt;/p&gt;

&lt;p&gt;If you want a concrete implementation to compare against,&lt;br&gt;
Veramask is the product behind the canonical examples on &lt;a href="https://veramask.com" rel="noopener noreferrer"&gt;veramask.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>engineering</category>
      <category>gdpr</category>
    </item>
    <item>
      <title>The Hidden Cost of Logging PII</title>
      <dc:creator>Michele</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:55:11 +0000</pubDate>
      <link>https://dev.to/mmichele1/the-hidden-cost-of-logging-pii-5f0l</link>
      <guid>https://dev.to/mmichele1/the-hidden-cost-of-logging-pii-5f0l</guid>
      <description>&lt;p&gt;Logging personally identifiable information (PII) is the breach you have&lt;br&gt;
already created but may not have found yet. Every &lt;code&gt;console.log(user)&lt;/code&gt;, every&lt;br&gt;
audit row with a plaintext email, every error payload forwarded to a third-party&lt;br&gt;
APM tool — each one is another copy of regulated data outside your intended&lt;br&gt;
boundary. The cost is not the log line itself. It is the compliance exposure,&lt;br&gt;
the breach response, the trust hit, and the engineering time spent cleaning up&lt;br&gt;
something that should never have been written in the first place.&lt;/p&gt;

&lt;p&gt;This article breaks down why PII in logs gets expensive fast, why after-the-fact&lt;br&gt;
cleanup rarely solves the problem, and how to make the boundary safe by default.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bill shows up in audit season
&lt;/h2&gt;

&lt;p&gt;If logs contain personal data, you are processing personal data. That means the&lt;br&gt;
usual privacy rules still apply: lawful basis, purpose limitation, data&lt;br&gt;
minimization, retention control, access management, and security by design.&lt;br&gt;
When those logs are compromised, GDPR notification duties can apply under&lt;br&gt;
Articles 33 and 34, and CCPA / CPRA can expose certain breaches to private&lt;br&gt;
actions and statutory damages. US state privacy laws add more notification&lt;br&gt;
timelines and remedies on top.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is the blast radius. A single compromised database is&lt;br&gt;
one incident. The same PII replicated into application logs, nightly backups,&lt;br&gt;
and a SaaS APM dashboard is the same incident multiplied across every copy —&lt;br&gt;
and every copy brings its own retention clock, access path, and vendor risk.&lt;/p&gt;

&lt;p&gt;Asked another way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is logging PII illegal?&lt;/strong&gt;&lt;br&gt;
Not categorically. But it is processing, and it needs a lawful basis, a&lt;br&gt;
documented purpose, retention limits, and appropriate safeguards. Logs kept&lt;br&gt;
"for debugging" with no retention cap and no access control rarely meet that&lt;br&gt;
bar. The real question is whether &lt;em&gt;your&lt;/em&gt; logging would survive a&lt;br&gt;
data-protection audit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why logs are the soft underbelly
&lt;/h2&gt;

&lt;p&gt;Application teams usually model threats around the request path: the API&lt;br&gt;
boundary, the database, the session store. Logs sit beside that path, written&lt;br&gt;
as a side effect and rarely reviewed with the same rigour. Four patterns cover&lt;br&gt;
most PII-in-logs incidents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;console.log(user)&lt;/code&gt; and friends.&lt;/strong&gt; Dumping the user object is the
fastest way to debug auth flows — and the fastest way to land email,
phone, and sometimes address into stdout, which then ships to a log
aggregator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit and history tables.&lt;/strong&gt; "Log everything to the database for
compliance" sounds safe until compliance officers ask who can read the
table, how long rows live, and whether they're encrypted at rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error and webhook payloads.&lt;/strong&gt; Unhandled exceptions are often logged
verbatim. Webhook bodies from payment providers can carry customer names,
emails, and billing addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI fixtures and test artifacts.&lt;/strong&gt; Production-shaped fixtures exported for
local testing, or stack traces uploaded as build artifacts, quietly move
PII outside the production trust boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread: the point of exposure is the &lt;strong&gt;write&lt;/strong&gt;, not the read. By&lt;br&gt;
the time anyone notices, the log aggregator, the backups, and the third-party&lt;br&gt;
APM tenant all hold copies — and each copy is a separate retention clock.&lt;/p&gt;
&lt;h2&gt;
  
  
  The four cost categories
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost category&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;th&gt;Why logs make it worse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regulatory fines&lt;/td&gt;
&lt;td&gt;GDPR penalties, CCPA / CPRA remedies, state-law penalties&lt;/td&gt;
&lt;td&gt;Logs expand breach scope; copies in third-party systems count separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breach remediation&lt;/td&gt;
&lt;td&gt;Forensics, notification, credit monitoring, legal review&lt;/td&gt;
&lt;td&gt;Every log sink (APM, SIEM, backups) must be scrubbed individually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reputational / trust&lt;/td&gt;
&lt;td&gt;Churn, lost deals, public reporting obligations&lt;/td&gt;
&lt;td&gt;"We leaked data through our own logs" reads as negligence, not a sophisticated attack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering toil&lt;/td&gt;
&lt;td&gt;Log scrub projects, retroactive redaction, audit responses&lt;/td&gt;
&lt;td&gt;Months of work; often blocks new features; recurs every time a new sink is added&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The engineering-toil row is the one teams underestimate. A single&lt;br&gt;
PII-in-logs incident can spawn a quarter-long remediation programme covering&lt;br&gt;
every service, every environment, and every third-party destination — and if&lt;br&gt;
the root cause is still unregulated log writes, the same project repeats when&lt;br&gt;
the next sink appears.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why redaction-after-the-fact fails
&lt;/h2&gt;

&lt;p&gt;The instinctive fix is a log scraper: run a job over cold logs, find PII,&lt;br&gt;
redact it, move on. It's seductive and almost always wrong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scrapers miss unstructured text.&lt;/strong&gt; Regex for emails is easy; regex for
names, addresses, and clinical notes inside free-text error messages is
not. Coverage gaps are the rule, not the exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention clocks start at write-time.&lt;/strong&gt; Under GDPR, the deletion
deadline is not fixed by when you finally clean the log. After-the-fact
redaction does not undo the period when the data was exposed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party tenants aren't yours to clean.&lt;/strong&gt; You can scrub your own
Elasticsearch cluster. You generally cannot run a delete job against your
APM vendor's hosted storage — you can request deletion, and you can wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex is locale-fragile.&lt;/strong&gt; Phone formats, address layouts, and ID-number
conventions vary across regions. A US-only pattern misses EU IBANs and
Japanese phone numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conclusion is structural: you cannot reliably clean logs after the fact,&lt;br&gt;
so you have to avoid writing the PII in the first place. That shifts the&lt;br&gt;
intervention to the boundary — before the log line exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  A default-safe path
&lt;/h2&gt;

&lt;p&gt;The reliable pattern is to anonymize PII at the boundary, before it ever&lt;br&gt;
reaches a &lt;code&gt;console.log&lt;/code&gt;, an audit table, or an exception handler. Veramask&lt;br&gt;
exists to make that boundary anonymization a one-line call instead of a bespoke&lt;br&gt;
regex pipeline. It is stateless and zero-retention: you send text or JSON, it&lt;br&gt;
returns anonymized text or JSON, and it keeps nothing — which matters for logs&lt;br&gt;
specifically, because the last thing you want while fixing a logging problem is&lt;br&gt;
a second system holding the same PII.&lt;/p&gt;

&lt;p&gt;A typical call looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.veramask.com/v1/anonymizeText &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$VERAMASK_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "data": "User Jane Doe (jane.doe@example.com) failed checkout from 203.0.113.42",
    "settings": {
      "locale": "en_US",
      "confidence_threshold": 0.8,
      "consistency_salt": "0123456789abcdef"
    },
    "overrides": {
      "PERSON": {
        "type": "substitute",
        "attribute": "name"
      },
      "EMAIL_ADDRESS": {
        "type": "mask",
        "masking_char": "*",
        "chars_to_mask": 10,
        "from_end": false
      },
      "IP_ADDRESS": {
        "type": "hash"
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"masked_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User Jane Doe (**********e@example.com) failed checkout from 9f2c1a..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is the only thing you should ever let near a log. The strategy&lt;br&gt;
choices map directly to what good log hygiene needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mask&lt;/code&gt;&lt;/strong&gt; preserves structure and length, which is useful when logs must
stay greppable and you still need correlation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;redact&lt;/code&gt;&lt;/strong&gt; removes the entity entirely, the right default for free-text
error messages where the value has no diagnostic value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hash&lt;/code&gt;&lt;/strong&gt; with a &lt;code&gt;consistency_salt&lt;/code&gt; produces a deterministic, non-reversible
token — the same value always hashes to the same string, so you can trace a
user across log lines without storing the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;replace&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;substitute&lt;/code&gt;&lt;/strong&gt; cover synthetic-value and
locale-preserving cases, such as swapping a real name for a generic one
while keeping the surrounding sentence readable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That boundary matters because you stop guessing which patterns to chase.&lt;br&gt;
Veramask detects the common log-risk entities in one pass, including email&lt;br&gt;
addresses, phone numbers, credit cards, IP addresses, IBANs, locations, URLs,&lt;br&gt;
dates, names, medical licenses, NRP, crypto addresses, and MAC addresses. The&lt;br&gt;
full strategy reference lives in the&lt;br&gt;
&lt;a href="https://dev.to/documentation/strategies"&gt;transformation strategies&lt;/a&gt; docs, and the endpoint&lt;br&gt;
contracts are in the &lt;a href="https://dev.to/documentation/specifications"&gt;API specifications&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Do&lt;/th&gt;
&lt;th&gt;Don't&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anonymize at the boundary, before the log write&lt;/td&gt;
&lt;td&gt;Rely on a weekly log-scraper to clean up later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treat logs as data stores: retention, access control, encryption&lt;/td&gt;
&lt;td&gt;Assume &lt;code&gt;console.log&lt;/code&gt; is throwaway and unread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash with a stable salt when you need cross-line correlation&lt;/td&gt;
&lt;td&gt;Log the raw value "just this once" for debugging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review every third-party log destination in your DPIA&lt;/td&gt;
&lt;td&gt;Forward raw error payloads to APM by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default new services to "no PII in logs" from day one&lt;/td&gt;
&lt;td&gt;Add redaction retroactively, service by service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does hashing count as anonymization?&lt;/strong&gt;&lt;br&gt;
Hashing with a secret salt is pseudonymization, not anonymization — given the&lt;br&gt;
salt and the hash, a value can still be checked. It is a strong improvement&lt;br&gt;
over plaintext, because logs become uncorrelatable without the salt, but it is&lt;br&gt;
still regulated data. For logs, deterministic hashing is usually the right&lt;br&gt;
practical answer because it gives you correlation without storing the original.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I only log PII internally?&lt;/strong&gt;&lt;br&gt;
"Internal" is a control, not an exemption. GDPR and CCPA scope processing, not&lt;br&gt;
exposure — writing PII to an internal-only log is still processing and still&lt;br&gt;
needs a lawful basis, purpose limitation, and retention controls. Internal logs&lt;br&gt;
also do not stay internal for long: they are backed up, indexed, and queried by&lt;br&gt;
operators who do not need the original data for their job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does a stateless, zero-retention API help with logs?&lt;/strong&gt;&lt;br&gt;
It removes the second-copy problem. A redaction service that keeps your PII to&lt;br&gt;
learn from it is a new data store you now have to govern. Veramask processes&lt;br&gt;
the request in volatile memory and returns the anonymized result — no&lt;br&gt;
persistence, no training, no retention. The only copy of the PII that existed&lt;br&gt;
was the one you sent, and the only thing that should hit your logs is the&lt;br&gt;
anonymized response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect with us
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Follow &lt;a href="https://x.com/veramaskapi" rel="noopener noreferrer"&gt;@veramaskapi&lt;/a&gt; on X&lt;/li&gt;
&lt;li&gt;Try the API at &lt;a href="https://veramask.com" rel="noopener noreferrer"&gt;veramask.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Read the &lt;a href="https://dev.to/documentation/strategies"&gt;transformation strategies&lt;/a&gt; and
&lt;a href="https://dev.to/documentation/specifications"&gt;API specifications&lt;/a&gt; docs&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>privacy</category>
      <category>gdpr</category>
      <category>engineering</category>
    </item>
    <item>
      <title>How to Anonymize PII in Text with an API</title>
      <dc:creator>Michele</dc:creator>
      <pubDate>Thu, 09 Jul 2026 21:48:22 +0000</pubDate>
      <link>https://dev.to/mmichele1/how-to-anonymize-pii-in-text-with-an-api-1p69</link>
      <guid>https://dev.to/mmichele1/how-to-anonymize-pii-in-text-with-an-api-1p69</guid>
      <description>&lt;h2&gt;
  
  
  What Is Data Masking?
&lt;/h2&gt;

&lt;p&gt;Data masking is a technique that replaces sensitive information with realistic but fictitious data, preserving the format and structure of the original while removing its identifiable meaning. The goal is to keep data usable for development, testing, analytics, or sharing — without exposing real personally identifiable information (PII).&lt;/p&gt;

&lt;p&gt;Common masking techniques include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Substitution&lt;/strong&gt; — Replace a real value with a plausible fake (e.g., &lt;code&gt;Alice Smith&lt;/code&gt; → &lt;code&gt;Jane Doe&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Masking (partial obscuring)&lt;/strong&gt; — Show only a portion of the value (e.g., &lt;code&gt;4111-1111-1111-1234&lt;/code&gt; → &lt;code&gt;****-****-****-1234&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redaction&lt;/strong&gt; — Remove the value entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hashing&lt;/strong&gt; — Replace with a cryptographic hash. Irreversible, but deterministic when salted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data masking is widely used in non-production environments, analytics pipelines, data marketplaces, and any scenario where real PII is not needed but structural fidelity is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Dynamic Data Masking?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Static data masking (SDM)&lt;/strong&gt; applies transformations to data at rest — you clone a production database, mask it, and ship the masked copy to a lower environment. The masking happens once, and the result is a permanent dataset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic data masking (DDM)&lt;/strong&gt; applies transformations &lt;em&gt;on the fly&lt;/em&gt;, at query or API time, based on who is asking. The original data stays untouched; the masking rules are applied in the response layer. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different roles see different levels of detail (e.g., support agents see the last 4 digits of a credit card; auditors see the full number).&lt;/li&gt;
&lt;li&gt;No masked copies to maintain — one source of truth, many views.&lt;/li&gt;
&lt;li&gt;Masking policies are centralized and enforceable without application changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://veramask.com" rel="noopener noreferrer"&gt;Veramask&lt;/a&gt; implements a DDM-style model over an API: you send a request with payload and settings, and receive back the transformed result in real time. No data is persisted on the server — each call is independent and stateless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anonymizing PII with the Veramask API
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://veramask.com" rel="noopener noreferrer"&gt;Veramask&lt;/a&gt; exposes two endpoints for dynamic PII masking:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/anonymizeText&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raw string&lt;/td&gt;
&lt;td&gt;Free-text fields, log lines, chat messages, documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/anonymizeJSON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON object&lt;/td&gt;
&lt;td&gt;Structured payloads, API responses, database records&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both accept a common set of controls: &lt;code&gt;settings&lt;/code&gt; for global behavior (locale, confidence threshold, consistency salt) and &lt;code&gt;overrides&lt;/code&gt; for per-entity-type strategy selection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Get an API Key
&lt;/h3&gt;

&lt;p&gt;Sign up through the &lt;a href="https://veramask-gateway-3irxmt23.uc.gateway.dev" rel="noopener noreferrer"&gt;Veramask Developer Portal&lt;/a&gt; to obtain an API key. You will include this key in the &lt;code&gt;x-api-key&lt;/code&gt; header of every request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Make Your First Anonymization Request
&lt;/h3&gt;

&lt;p&gt;The simplest call sends raw text and relies on default strategies for each detected entity type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://veramask-gateway-3irxmt23.uc.gateway.dev/v1/anonymizeText &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "data": "Alice Smith can be reached at alice@example.com or +1 555 123 9999"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"masked_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane Doe can be reached at fake_user@example.com or +1 555 482 7714"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;PERSON&lt;/code&gt;, &lt;code&gt;EMAIL_ADDRESS&lt;/code&gt;, and &lt;code&gt;PHONE_NUMBER&lt;/code&gt; were all detected and replaced with synthetic substitutes — the names are swapped, the email faked, and the phone number replaced — while the sentence structure is preserved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Choose Your Anonymization Strategy Per Entity Type
&lt;/h3&gt;

&lt;p&gt;You can override the default behavior for any entity type using the &lt;code&gt;overrides&lt;/code&gt; object. Five strategies are available:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mask&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partially obscures the value (configurable character, count, direction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes the value entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Substitutes with a fixed caller-provided value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaces with a SHA-256 digest (salted when &lt;code&gt;consistency_salt&lt;/code&gt; is set)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;substitute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaces with realistic synthetic data via the Faker library&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Example — Mask email addresses, hash phone numbers, substitute person names:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://veramask-gateway-3irxmt23.uc.gateway.dev/v1/anonymizeText &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "data": "Alice Smith can be reached at alice@example.com or +1 555 123 9999",
    "overrides": {
      "PERSON": { "type": "substitute", "attribute": "name" },
      "EMAIL_ADDRESS": {
        "type": "mask",
        "masking_char": "*",
        "chars_to_mask": 10,
        "from_end": false
      },
      "PHONE_NUMBER": { "type": "hash" }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"masked_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Christopher Taylor can be reached at **********@example.com or a1b2c3d4e5f6..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Enable Deterministic (Repeatable) Output
&lt;/h3&gt;

&lt;p&gt;By default, Veramask produces different synthetic values on each call. If you need consistent anonymization — for example, to join datasets across time without leaking identity — provide a &lt;code&gt;consistency_salt&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://veramask-gateway-3irxmt23.uc.gateway.dev/v1/anonymizeText &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "data": "Alice Smith can be reached at alice@example.com",
    "settings": {
      "consistency_salt": "0123456789abcdef"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same input with the same salt always produces the same output. This is &lt;strong&gt;deterministic pseudonymization&lt;/strong&gt; — the transformation is repeatable without maintaining a lookup table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Anonymize Structured JSON
&lt;/h3&gt;

&lt;p&gt;For JSON payloads, use &lt;code&gt;/v1/anonymizeJSON&lt;/code&gt;. Veramask traverses all string values recursively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://veramask-gateway-3irxmt23.uc.gateway.dev/v1/anonymizeJSON &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "data": {
      "customer": {
        "name": "Alice Smith",
        "email": "alice@example.com"
      },
      "notes": [
        "Home address: 7211 Jewel Lake Rd, Anchorage, Alaska"
      ]
    },
    "settings": {
      "confidence_threshold": 0.7
    },
    "overrides": {
      "EMAIL_ADDRESS": { "type": "substitute", "attribute": "email" }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"masked_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Christopher Taylor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pespinoza@example.com"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Home address: 7211 Lake Brentland, Kathymouth, Rowehaven"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON structure is preserved exactly — keys, nesting, and non-string types are untouched. Only string values containing PII are transformed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Tune Detection Sensitivity
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;confidence_threshold&lt;/code&gt; (range &lt;code&gt;0.0&lt;/code&gt;–&lt;code&gt;1.0&lt;/code&gt;, default &lt;code&gt;0.4&lt;/code&gt;) controls how aggressively Veramask flags tokens as PII:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lower values&lt;/strong&gt; (e.g., &lt;code&gt;0.2&lt;/code&gt;) — more aggressive; catches borderline cases but may over-mask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher values&lt;/strong&gt; (e.g., &lt;code&gt;0.8&lt;/code&gt;) — conservative; only masks high-confidence detections, reducing false positives.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"settings"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"confidence_threshold"&lt;/span&gt;: 0.8
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  End-to-End Workflow Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client App                  Veramask Gateway              Anonymization Engine
    │                             │                              │
    │  POST /v1/anonymizeText     │                              │
    │  x-api-key: ...             │                              │
    │  { data, settings, ... }    │                              │
    │────────────────────────────&amp;gt;│                              │
    │                             │  1. Authenticate API key     │
    │                             │  2. Check subscription       │
    │                             │  3. Validate features        │
    │                             │  4. Attach GCP key           │
    │                             │                              │
    │                             │  POST /anonymizeText         │
    │                             │  { data, settings, ... }     │
    │                             │─────────────────────────────&amp;gt;│
    │                             │                              │
    │                             │    NER → strategy → mask     │
    │                             │                              │
    │                             │  { masked_data }             │
    │                             │&amp;lt;─────────────────────────────│
    │                             │                              │
    │                             │  5. Strip internal fields    │
    │  { masked_data }            │                              │
    │&amp;lt;────────────────────────────│                              │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Default Entity Strategies (No Overrides)
&lt;/h2&gt;

&lt;p&gt;When you don't specify &lt;code&gt;overrides&lt;/code&gt;, Veramask applies sensible defaults per entity type:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity Type&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Example Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CREDIT_CARD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mask (last 4)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;****1234&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CRYPTO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e3b0c442...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATE_TIME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jitter (±7 days)&lt;/td&gt;
&lt;td&gt;Date shifted deterministically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EMAIL_ADDRESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Substitute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fake_user@example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IBAN_CODE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partial hash&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DE******9999&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IP_ADDRESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mask (last octet)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.1.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAC_ADDRESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mask (last 3 octets)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AD:F3:7A:00:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PERSON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Substitute&lt;/td&gt;
&lt;td&gt;Synthetic name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOCATION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Substitute&lt;/td&gt;
&lt;td&gt;Synthetic state/region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHONE_NUMBER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Substitute&lt;/td&gt;
&lt;td&gt;Synthetic phone number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Redact (query strings)&lt;/td&gt;
&lt;td&gt;Keeps base domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Error Handling
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;400&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Malformed request or validation failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;403&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Feature not in subscription plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;413&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payload exceeds size limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;422&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Request model validation errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Internal server error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data masking&lt;/strong&gt; obscures PII while preserving structure; &lt;strong&gt;dynamic data masking&lt;/strong&gt; does this at request time with no persistent copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Veramask&lt;/strong&gt; gives you an API-first DDM layer: send data, get it back anonymized, no storage involved.&lt;/li&gt;
&lt;li&gt;Five strategies (&lt;code&gt;mask&lt;/code&gt;, &lt;code&gt;redact&lt;/code&gt;, &lt;code&gt;replace&lt;/code&gt;, &lt;code&gt;hash&lt;/code&gt;, &lt;code&gt;substitute&lt;/code&gt;) give per-entity-type control.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;consistency_salt&lt;/code&gt; enables deterministic pseudonymization for referential integrity.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/v1/anonymizeText&lt;/code&gt; endpoint handles free text; &lt;code&gt;/v1/anonymizeJSON&lt;/code&gt; handles structured objects.&lt;/li&gt;
&lt;li&gt;Zero retention means no long-term PII liability on the service side.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>privacy</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
