<?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: Veramask API Team</title>
    <description>The latest articles on DEV Community by Veramask API Team (@veramask).</description>
    <link>https://dev.to/veramask</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%2F12f9dbd5-1294-4970-bbd7-2fd48a5cf393.png</url>
      <title>DEV Community: Veramask API Team</title>
      <link>https://dev.to/veramask</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/veramask"/>
    <language>en</language>
    <item>
      <title>Passing a data protection audit with a stateless API</title>
      <dc:creator>Veramask API Team</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/veramask/passing-a-data-protection-audit-with-a-stateless-api-3apa</link>
      <guid>https://dev.to/veramask/passing-a-data-protection-audit-with-a-stateless-api-3apa</guid>
      <description>&lt;p&gt;Data protection audits rarely fail on the database you secured. They fail on&lt;br&gt;
the copies you forgot: the log aggregator, the nightly backup, the APM&lt;br&gt;
tenant, the CRM sync, the export someone made for a migration and never&lt;br&gt;
deleted. An audit is a walk through every place personal data lives, and&lt;br&gt;
every copy that isn't on your map — with a retention period, an access&lt;br&gt;
policy, and an owner — is a finding. The stronger your core system looks, the&lt;br&gt;
more these side copies stick out.&lt;/p&gt;

&lt;p&gt;This article covers what a data protection audit actually asks for, the&lt;br&gt;
copy-multiplication pattern that fails them, and how placing a stateless&lt;br&gt;
anonymization boundary in front of your writes changes the conversation:&lt;br&gt;
what it removes from audit scope, what it deliberately does &lt;em&gt;not&lt;/em&gt; remove, and&lt;br&gt;
the evidence you should have in the room when the auditor arrives.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a data protection audit actually covers
&lt;/h2&gt;

&lt;p&gt;The material is predictable. Under GDPR — and mirrored by CCPA/CPRA and the&lt;br&gt;
US state privacy laws in slightly different vocabulary — an audit walks&lt;br&gt;
seven areas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;What the auditor asks for&lt;/th&gt;
&lt;th&gt;Where it comes from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Records of processing (Art 30)&lt;/td&gt;
&lt;td&gt;A per-purpose inventory: what data, why, where it flows, how long&lt;/td&gt;
&lt;td&gt;Your RoPA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-flow maps&lt;/td&gt;
&lt;td&gt;Every system and copy a record touches, including processors&lt;/td&gt;
&lt;td&gt;Architecture diagrams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lawful basis per activity&lt;/td&gt;
&lt;td&gt;Each processing purpose mapped to a basis (contract, consent, legitimate interest)&lt;/td&gt;
&lt;td&gt;Privacy policy + RoPA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention evidence&lt;/td&gt;
&lt;td&gt;Concrete periods per store and the mechanism that enforces deletion&lt;/td&gt;
&lt;td&gt;Retention schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DSAR readiness&lt;/td&gt;
&lt;td&gt;How access, erasure, and portability requests execute end-to-end, with timings&lt;/td&gt;
&lt;td&gt;Runbook + endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breach readiness&lt;/td&gt;
&lt;td&gt;The 72-hour notification process (Art 33), roles, decision trees, templates&lt;/td&gt;
&lt;td&gt;Incident response plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processor terms (Art 28)&lt;/td&gt;
&lt;td&gt;A DPA per processor and a disclosed sub-processor list&lt;/td&gt;
&lt;td&gt;Contracts + privacy policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of this is exotic. The failure mode is not ignorance of the list — it is&lt;br&gt;
that the honest answers to it sprawl faster than the documentation tracks&lt;br&gt;
them. Every new log sink, SaaS integration, and backup tier adds rows to&lt;br&gt;
three of those seven areas at once.&lt;/p&gt;
&lt;h2&gt;
  
  
  The pattern of failure: unmapped copies
&lt;/h2&gt;

&lt;p&gt;The typical finding sequence looks like this. The auditor asks to see every&lt;br&gt;
system holding customer personal data. You show the application database —&lt;br&gt;
encrypted, access-controlled, retention-timed, documented. Then they ask&lt;br&gt;
about logs, and the room gets quieter. The application writes request bodies&lt;br&gt;
to stdout; the aggregator holds them for 30 days; the nightly backup holds&lt;br&gt;
them for a year; the error tracker holds the payloads that crashed the&lt;br&gt;
webhook handler, sometimes with looser access than production. Four stores,&lt;br&gt;
four retention clocks, four access paths — and one of them is a third party&lt;br&gt;
whose storage you don't control.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Copy&lt;/th&gt;
&lt;th&gt;Why it exists&lt;/th&gt;
&lt;th&gt;Why it's a finding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Log aggregator&lt;/td&gt;
&lt;td&gt;Debugging and observability&lt;/td&gt;
&lt;td&gt;Plaintext PII with a retention period set by ops convention, not policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly backups&lt;/td&gt;
&lt;td&gt;Disaster recovery&lt;/td&gt;
&lt;td&gt;Deletion requests don't reach backups; the retention clock is the backup cycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APM / error tracker&lt;/td&gt;
&lt;td&gt;Exception triage&lt;/td&gt;
&lt;td&gt;Error payloads carry request context; hosted tenant you can't scrub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRM / analytics sync&lt;/td&gt;
&lt;td&gt;Business tooling&lt;/td&gt;
&lt;td&gt;A processor copy governed by a DPA you may not have signed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The deeper problem is that none of these copies was a &lt;em&gt;decision&lt;/em&gt;. Nobody&lt;br&gt;
chose to put PII in the aggregator — the aggregator ingests whatever the app&lt;br&gt;
writes, and the app writes what it logs. As&lt;br&gt;
&lt;a href="https://veramask.com/articles/the-hidden-cost-of-logging-pii" rel="noopener noreferrer"&gt;the hidden cost of logging PII&lt;/a&gt;&lt;br&gt;
argues, the point of exposure is the write, not the read. After-the-fact&lt;br&gt;
scraping can't reliably fix it, and you generally cannot run a delete job&lt;br&gt;
against a vendor's hosted storage. The control that works is upstream of the&lt;br&gt;
write.&lt;/p&gt;
&lt;h2&gt;
  
  
  The subtractive move: anonymize at the boundary
&lt;/h2&gt;

&lt;p&gt;Most privacy controls are &lt;em&gt;additive&lt;/em&gt;: encryption at rest, access control,&lt;br&gt;
retention timers, audit logging. Each layers on top of a system that still&lt;br&gt;
holds the data — each reduces risk, none reduces it to zero, because the data&lt;br&gt;
is still there.&lt;/p&gt;

&lt;p&gt;Boundary anonymization is &lt;em&gt;subtractive&lt;/em&gt;. Sanitize the payload before the&lt;br&gt;
write and the copy never contains the personal data — so it never needs its&lt;br&gt;
own retention clock, its own DSAR procedure, or its own line in the breach&lt;br&gt;
analysis. The copy drops off the audit's map entirely.&lt;/p&gt;

&lt;p&gt;The mechanism in this stack is a stateless anonymization API: you send text&lt;br&gt;
or JSON, it returns the same structure with sensitive entities replaced, and&lt;br&gt;
it retains nothing — no payload store, no input logging, no training corpus.&lt;br&gt;
The five-clause contract behind that claim (no persistent store, no input&lt;br&gt;
logging, no training on customer traffic, no cross-tenant state, memory as&lt;br&gt;
the only medium) is laid out in&lt;br&gt;
&lt;a href="https://veramask.com/articles/why-stateless-anonymization-reduces-exposure" rel="noopener noreferrer"&gt;why stateless anonymization reduces exposure&lt;/a&gt;.&lt;br&gt;
Here, the question is what that contract does to an audit.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a stateless boundary removes from audit scope
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The anonymization step never appears as a data store.&lt;/strong&gt; Your data-flow
map gains a transformer, not a box holding regulated data. Diagrams stay
small; the RoPA entry for the redaction leg is one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No retention clock on the redaction leg.&lt;/strong&gt; A store that retains nothing
has no retention period to document, no deletion job to demonstrate, and no
backup tier to explain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No vendor-storage DSAR dependency.&lt;/strong&gt; Erasure requests don't generate a
ticket to a redaction vendor's warehouse, because there is no warehouse.
Your DSAR runbook stays inside systems you control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No vendor-storage breach scenario.&lt;/strong&gt; The blast radius of a vendor
incident ends at non-content operational metadata — timestamps, request
sizes, processing durations, detected entity types. Painful for the vendor,
but not a personal-data breach on your side of the contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The DPIA stays proportionate.&lt;/strong&gt; A processing leg with no storage, no
profiling, and no persistence is a low-risk leg. Your assessment effort
concentrates where the originals actually live.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What it does not remove
&lt;/h2&gt;

&lt;p&gt;This section is the one to read twice, because over-claiming is how&lt;br&gt;
subtractive controls get written up as findings themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The processor still goes in your sub-processor list and RoPA.&lt;/strong&gt; Zero
retention changes the &lt;em&gt;description&lt;/em&gt; of the processing, not the obligation
to disclose it. "Anonymization API, zero-retention, volatile-memory
processing" is the entry — not silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Art 28 DPA is still required.&lt;/strong&gt; A processor acting on your behalf
needs a data processing agreement whether it keeps data for three years or
three hundred milliseconds. Retention length is a term &lt;em&gt;inside&lt;/em&gt; the DPA,
not an exemption from having one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You still need a lawful basis for sending the data through.&lt;/strong&gt; The call
itself is processing: personal data in transit is personal data. The basis
that lets you process the payload in the first place covers the call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pseudonymized output is still regulated.&lt;/strong&gt; Salted hashes and stable
synthetic mappings are pseudonymisation — re-linkable with additional
information you hold (the salt), so still personal data under Recital 26.
Truly irreversible output can exit scope, but know &lt;em&gt;which&lt;/em&gt; transformation
produced each field before you claim it. The
&lt;a href="https://veramask.com/articles/deterministic-anonymization-join-keys" rel="noopener noreferrer"&gt;deterministic anonymization guide&lt;/a&gt;
covers where that line sits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational metadata still gets a line.&lt;/strong&gt; Non-content metrics are not the
payload, but "timestamps, sizes, durations, entity types" is still a
sentence in your RoPA entry. Disclose it precisely and it's unremarkable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything upstream keeps its Article 32 measures.&lt;/strong&gt; Your database of
originals still needs encryption, access control, and breach detection —
all of it. The subtractive control is for the &lt;em&gt;copies&lt;/em&gt;; the system of
record keeps every layer it always had.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The auditor checklist
&lt;/h2&gt;

&lt;p&gt;The core deliverable: map each standard question to the evidence, and note&lt;br&gt;
where the boundary changes the answer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Auditor asks&lt;/th&gt;
&lt;th&gt;Evidence to have ready&lt;/th&gt;
&lt;th&gt;Where the stateless boundary helps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Show me every system holding customer PII"&lt;/td&gt;
&lt;td&gt;Data-flow map + RoPA&lt;/td&gt;
&lt;td&gt;Sinks written post-boundary hold tokens, not PII — the map stays small and true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"How long is data retained in each store?"&lt;/td&gt;
&lt;td&gt;Retention schedule per store&lt;/td&gt;
&lt;td&gt;The anonymization leg adds no clock; each sink's own policy still applies to its tokenized content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Walk me through a DSAR end-to-end"&lt;/td&gt;
&lt;td&gt;Runbook + export/erasure endpoints&lt;/td&gt;
&lt;td&gt;Fewer copies to search and purge; no dependency on a redaction vendor's storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What happens in a breach?"&lt;/td&gt;
&lt;td&gt;Incident plan, Art 33 timeline, comms templates&lt;/td&gt;
&lt;td&gt;Copies that were never written can't be breached — the notification analysis for those legs is empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Who processes data on your behalf?"&lt;/td&gt;
&lt;td&gt;Sub-processor list + DPAs&lt;/td&gt;
&lt;td&gt;The anonymization API is listed as a zero-retention processor; describe exactly that&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Where do logs fit?"&lt;/td&gt;
&lt;td&gt;Log retention policy + scrubbing evidence&lt;/td&gt;
&lt;td&gt;Logs written post-boundary contain tokens; the evidence is the anonymization configuration on the write path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Show me your DPIA"&lt;/td&gt;
&lt;td&gt;DPIA or documented low-risk assessment&lt;/td&gt;
&lt;td&gt;The redaction leg has no storage to assess; risk concentrates where originals live&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  A before-and-after: one webhook, four copies
&lt;/h2&gt;

&lt;p&gt;Take a payment webhook. The provider posts a payload with the customer's&lt;br&gt;
name and email; your handler writes it to the audit table, the log&lt;br&gt;
aggregator picks up the structured log line, the error tracker captures&lt;br&gt;
failures, and the nightly backup sweeps the database. One event, four&lt;br&gt;
stores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without a boundary:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stores holding the email&lt;/td&gt;
&lt;td&gt;4 (audit table, aggregator, error tracker, backup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention clocks&lt;/td&gt;
&lt;td&gt;4, set by four different policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DSAR effort&lt;/td&gt;
&lt;td&gt;Search and purge 4 systems; file a deletion request with the tracker vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breach analysis&lt;/td&gt;
&lt;td&gt;All 4 copies in scope; each needs its own notification assessment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;With a boundary at ingestion&lt;/strong&gt; — one call to &lt;code&gt;anonymizeJSON&lt;/code&gt; before&lt;br&gt;
anything is written:&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/anonymizeJSON &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": {
      "event": "subscription.updated",
      "customer": { "name": "Jane Doe", "email": "jane.doe@example.com" },
      "note": "Called from 203.0.113.42 about invoice 4711"
    },
    "settings": {
      "locale": "en_US",
      "consistency_salt": "0123456789abcdef"
    },
    "overrides": {
      "PERSON": { "type": "substitute", "attribute": "name" },
      "EMAIL_ADDRESS": { "type": "hash" },
      "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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subscription.updated"&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;"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;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Called from 8f223fc... about invoice 4711"&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 structure is preserved — non-string fields pass through, arrays and&lt;br&gt;
objects are traversed, downstream parsers keep working — but the four stores&lt;br&gt;
now hold a synthetic name and a stable hash token instead of the original.&lt;br&gt;
The &lt;code&gt;consistency_salt&lt;/code&gt; keeps the token deterministic, so support can still&lt;br&gt;
correlate a ticket to a subscription without the email existing in any copy;&lt;br&gt;
that mechanism is covered in&lt;br&gt;
&lt;a href="https://veramask.com/articles/deterministic-anonymization-join-keys" rel="noopener noreferrer"&gt;deterministic anonymization: join keys without storing PII&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stores holding the email&lt;/td&gt;
&lt;td&gt;0 — the email exists only in transit and in the system of record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention clocks&lt;/td&gt;
&lt;td&gt;The four stores' policies still apply, but to non-personal data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DSAR effort&lt;/td&gt;
&lt;td&gt;Those four legs are one line: "no personal data retained"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breach analysis&lt;/td&gt;
&lt;td&gt;Nothing personal on that path to notify about; the salt is the only secret, and it lives in your secret manager&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;Do I still need a DPA with a zero-retention vendor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. GDPR Article 28 governs any processor acting on your behalf; retention&lt;br&gt;
length is negotiated &lt;em&gt;inside&lt;/em&gt; that agreement, not a pass on signing it. A&lt;br&gt;
stateless anonymization vendor is the easiest DPA you'll sign — the&lt;br&gt;
processing description is short and the sub-processor list is short — but&lt;br&gt;
sign it, file it, and list the vendor in your privacy policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is anonymized output outside GDPR scope?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends which transformation produced it. &lt;code&gt;redact&lt;/code&gt; and &lt;code&gt;replace&lt;/code&gt; remove&lt;br&gt;
the original outright, and unsalted &lt;code&gt;substitute&lt;/code&gt; emits fictional values with&lt;br&gt;
no derivable link — candidates for true anonymization under Recital 26's&lt;br&gt;
"reasonably likely means" test. Salted &lt;code&gt;hash&lt;/code&gt; and salted &lt;code&gt;substitute&lt;/code&gt; are&lt;br&gt;
pseudonymisation: you hold the salt, so the link to the original exists, and&lt;br&gt;
the output remains personal data. Audit your overrides field by field and&lt;br&gt;
document each one's classification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What evidence should I keep for the anonymization control?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Four artifacts: the anonymization configuration (which entity types get which&lt;br&gt;
strategies, on which write paths), the salt governance policy if you&lt;br&gt;
pseudonymize, the processor list entry plus signed DPA, and the one-line RoPA&lt;br&gt;
description of the zero-retention processing. That packet answers every&lt;br&gt;
question in the checklist above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a stateless API replace encryption and access controls?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. It is subtractive for the copies; the systems that still hold originals —&lt;br&gt;
your database, your backups of it — keep every Article 32 measure they always&lt;br&gt;
had. The pattern is: subtract where the data need not exist, layer security&lt;br&gt;
where it must.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this help with CCPA and US state privacy laws too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mechanics map directly. Right-to-know and right-to-delete get easier with&lt;br&gt;
fewer copies to search; "reasonable security" is easier to demonstrate when&lt;br&gt;
the side copies hold tokens; and breach notification scope shrinks along with&lt;br&gt;
the copies. Different statutes, same arithmetic: less data stored, less to&lt;br&gt;
answer for.&lt;/p&gt;




&lt;p&gt;Originally published at&lt;br&gt;
&lt;a href="https://veramask.com/articles/passing-data-protection-audit-stateless-api" rel="noopener noreferrer"&gt;veramask.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gdpr</category>
      <category>privacy</category>
      <category>compliance</category>
    </item>
    <item>
      <title>How to anonymize data without breaking your analytics</title>
      <dc:creator>Veramask API Team</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/veramask/how-to-anonymize-data-without-breaking-your-analytics-m2g</link>
      <guid>https://dev.to/veramask/how-to-anonymize-data-without-breaking-your-analytics-m2g</guid>
      <description>&lt;p&gt;Your privacy team ships anonymization. A week later, your analytics team&lt;br&gt;
files a ticket: daily active users dropped 90%, the conversion funnel leaks&lt;br&gt;
at every step, and duplicate detection stopped firing. Nobody did anything&lt;br&gt;
wrong — anonymization did its privacy job, and in the same pass it destroyed&lt;br&gt;
every join key in the dataset. The same person now looks like a different&lt;br&gt;
person on every request, because each call generated a fresh fake value.&lt;/p&gt;

&lt;p&gt;The fix is a pattern, not a product: make the anonymization &lt;strong&gt;deterministic&lt;br&gt;
with a salt you keep&lt;/strong&gt;. Same input plus same salt equals same output — every&lt;br&gt;
call, every service, every environment. The masked value becomes a stable&lt;br&gt;
handle for the record it came from, so you can group, join, and trace exactly&lt;br&gt;
as you did before, without storing any original value and without running a&lt;br&gt;
tokenization vault. This article walks the pattern, the pseudocode, the salt&lt;br&gt;
governance, and the cases where you should &lt;em&gt;not&lt;/em&gt; use it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why anonymization breaks analytics
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you were doing&lt;/th&gt;
&lt;th&gt;What happens with random anonymization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GROUP BY user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One real user fragments into N synthetic users — counts inflate, averages smear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessionization and funnels&lt;/td&gt;
&lt;td&gt;Steps can't be linked to one subject; funnels leak at every stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-service tracing&lt;/td&gt;
&lt;td&gt;Service A's log and service B's event carry different fake values for the same person&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate detection&lt;/td&gt;
&lt;td&gt;Every copy of a record looks unique; dedupe never fires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Joining logs to a warehouse&lt;/td&gt;
&lt;td&gt;No shared key on either side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Top users by activity"&lt;/td&gt;
&lt;td&gt;Rankings become noise; heavy users never concentrate on one token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The underlying issue: the anonymized value was carrying the identity.&lt;br&gt;
Destroying identity is anonymization's &lt;em&gt;privacy job&lt;/em&gt; — the mistake is&lt;br&gt;
applying it where identity was also the &lt;em&gt;analytical key&lt;/em&gt;. Determinism&lt;br&gt;
restores the key while never emitting the original value. The token is a&lt;br&gt;
handle to the record, not the record.&lt;/p&gt;
&lt;h2&gt;
  
  
  The pattern: salted determinism
&lt;/h2&gt;

&lt;p&gt;No memory, no vault, no coordination service. You pick a secret salt, keep&lt;br&gt;
it in your configuration, and pass it to the anonymization function. The&lt;br&gt;
function mixes the salt into the transformation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;hash&lt;/strong&gt; computes a salted SHA-256 of the value. Same input + same salt →
the same 64-hex-character token, every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;substitute&lt;/strong&gt; picks the synthetic value as a deterministic function of the
input and the salt. Same input + same salt → the same fake name, email, or
phone number, every time.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The whole pattern in one call:&lt;/span&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="c1"&gt;// &amp;gt;= 16 bytes, from your secret manager&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;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;The salt is the only cross-request link, and it is yours. Two services that&lt;br&gt;
share nothing else — no shared database, no token service, no coordination&lt;br&gt;
calls — derive the same token for the same value, because both run the same&lt;br&gt;
function with the same salt. Correlation without a vault.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hash tokens as join keys
&lt;/h2&gt;

&lt;p&gt;The classic use: sanitize logs for PII while keeping them traceable.&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) failed checkout from 203.0.113.42&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;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;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;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;span class="c1"&gt;// → "User Jane Doe (9f2c1a...) failed checkout from 8f223fc..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every log line that mentions that email now carries the same &lt;code&gt;9f2c1a...&lt;/code&gt;&lt;br&gt;
token. Grep for the token and you have the user's complete trail — without&lt;br&gt;
the email existing in any line. Put the same salt in another service's&lt;br&gt;
config and it computes the same token independently: cross-service joins&lt;br&gt;
with zero infrastructure between them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Stable synthetic values
&lt;/h2&gt;

&lt;p&gt;Hashes are ugly in datasets humans read. Salted &lt;code&gt;substitute&lt;/code&gt; gives you the&lt;br&gt;
same determinism in realistic shape:&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 alice@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;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;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;name&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;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;span class="c1"&gt;// → "Christopher Taylor can be reached at pespinoza@example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every occurrence of &lt;code&gt;Alice Smith&lt;/code&gt; in the salted corpus becomes&lt;br&gt;
&lt;code&gt;Christopher Taylor&lt;/code&gt;; every occurrence of her email becomes&lt;br&gt;
&lt;code&gt;pespinoza@example.com&lt;/code&gt;. A staging database seeded this way keeps working:&lt;br&gt;
group-bys return stable per-user rows, and the data still looks like data.&lt;/p&gt;

&lt;p&gt;One caveat: substitution does not preserve relationships &lt;em&gt;between&lt;/em&gt; fields.&lt;br&gt;
The fake name and the fake email are each individually stable, but they do&lt;br&gt;
not describe the same fictional person. If you need referential integrity&lt;br&gt;
across entity types, use the hash as the join key and substitute only for&lt;br&gt;
human-readable surfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Salt governance: the salt is a credential
&lt;/h2&gt;

&lt;p&gt;Determinism moves the secret from a database into the salt. Treat it like&lt;br&gt;
one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generate it randomly&lt;/strong&gt;, store it in a secret manager — never in the
repo, never in code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never log it next to the tokens it produces.&lt;/strong&gt; A salt published beside
its hashes turns every pseudonym into a verifiable guess target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope it per purpose.&lt;/strong&gt; One salt for log sanitization, a different one
for the dataset you share externally. Tokens from one scope can't be
correlated with the other, and one leak doesn't compromise everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate it knowing the cost.&lt;/strong&gt; A new salt re-baselines every token; new
values stop matching historical ones. Version the generation (a
&lt;code&gt;token_epoch&lt;/code&gt; column) so consumers know which era a token belongs to, and
destroy the old salt once the migration is done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting the salt is the off switch.&lt;/strong&gt; With it gone, tokens can no longer
be verified against candidate values — the pseudonymization link you
created is severed, which is exactly what you want when the analytical
purpose expires.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When not to salt
&lt;/h2&gt;

&lt;p&gt;Determinism is a default for pipelines, not a law of nature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small populations.&lt;/strong&gt; A stable token per subject preserves cardinality
and frequency. In a five-user dataset, the token &lt;em&gt;is&lt;/em&gt; the user — context
alone re-identifies them. If a recurring fake value would itself be a
leak, you want per-request randomness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-cardinality fields.&lt;/strong&gt; Fifty states map to fifty tokens; frequency
analysis matches them in minutes. Deterministic substitution of location
fields buys nothing analytically and confirms structure to an attacker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-shot jobs.&lt;/strong&gt; A single log line that will never be joined to anything
gains nothing from a salt — and every salt you manage is a credential with
a rotation cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness is the point.&lt;/strong&gt; Demo screenshots, red-team fixtures, and
load-test payloads often want &lt;em&gt;different&lt;/em&gt; fake data every run. That is
what unsalted anonymization gives you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A token vault, minus the vault
&lt;/h2&gt;

&lt;p&gt;The enterprise alternative is a tokenization vault: an encrypted store&lt;br&gt;
mapping originals to tokens, with a detokenization API for authorized&lt;br&gt;
callers. Vaults work — and they are a concentrated database of originals you&lt;br&gt;
must run, secure, back up, and audit forever. The salt gets you the same&lt;br&gt;
correlation property with a different architecture:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tokenization vault&lt;/th&gt;
&lt;th&gt;Salted determinism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;A database of original ↔ token mappings&lt;/td&gt;
&lt;td&gt;A secret plus a pure function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side store&lt;/td&gt;
&lt;td&gt;Yes — the vault holds every original&lt;/td&gt;
&lt;td&gt;None — nothing to store, nothing to breach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reversible&lt;/td&gt;
&lt;td&gt;Yes, by design (detokenization)&lt;/td&gt;
&lt;td&gt;hash: not decryptable — with the salt you can verify a guess, never recover the value. substitute: not derivable at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational cost&lt;/td&gt;
&lt;td&gt;Run, secure, back up, audit, scale the vault&lt;/td&gt;
&lt;td&gt;Keep one secret in the secret manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breach impact&lt;/td&gt;
&lt;td&gt;The vault is a single point of total PII compromise&lt;/td&gt;
&lt;td&gt;A leaked salt is a rotated credential; no data store sits behind it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR posture&lt;/td&gt;
&lt;td&gt;Pseudonymisation with added attack surface&lt;/td&gt;
&lt;td&gt;Pseudonymisation with subtractive storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you need the original back later — support workflows that must&lt;br&gt;
re-identify a customer — you want a vault or encryption, not anonymization.&lt;br&gt;
If you need correlation without recovery, the salt is the whole answer.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is a salted hash anonymous data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No — it is pseudonymisation. Because the salt plus the token lets you verify&lt;br&gt;
candidate values, GDPR treats the output as personal data; the regulation&lt;br&gt;
recognizes the technique, under Article 32(1)(a), as a &lt;em&gt;security measure&lt;/em&gt;&lt;br&gt;
rather than an exemption. If your goal is true anonymization — data that&lt;br&gt;
exits GDPR scope — the tools are deletion, aggregation, and noise, not&lt;br&gt;
hashing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when I rotate the salt?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every token changes. Treat rotation as a re-baseline: version the token&lt;br&gt;
generation so both eras stay queryable, or accept the cliff and reprocess&lt;br&gt;
what you can. Keep the old salt only for the migration window, then destroy&lt;br&gt;
it — a retired salt has no analytical value and nonzero risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use one salt for everything?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Technically yes; hygienically, scope per purpose. One salt means one leak&lt;br&gt;
compromises every dataset's linkability, and internal-log tokens could be&lt;br&gt;
correlated with tokens in a dataset you shared externally. Separate salts&lt;br&gt;
per purpose are free — more entries in the secret manager — and they cap the&lt;br&gt;
blast radius of any disclosure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do I get different outputs for the same input?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usual causes: no salt passed at all (unsalted substitutions and hashes vary&lt;br&gt;
by request — that is normal behavior); a different salt between calls; or&lt;br&gt;
the two calls ran with different strategy overrides. Confirm the exact same&lt;br&gt;
salt string and the same overrides are being sent every time before&lt;br&gt;
debugging anything else.&lt;/p&gt;




&lt;p&gt;If you want a concrete implementation of this pattern to compare against,&lt;br&gt;
Veramask — the PII anonymization API behind the canonical examples on&lt;br&gt;
&lt;a href="https://veramask.com" rel="noopener noreferrer"&gt;veramask.com&lt;/a&gt; — implements it as the &lt;code&gt;consistency_salt&lt;/code&gt;&lt;br&gt;
setting on its &lt;code&gt;hash&lt;/code&gt; and &lt;code&gt;substitute&lt;/code&gt; strategies, with the stateless&lt;br&gt;
contract that keeps the salt and the payloads out of any server-side store.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>analytics</category>
      <category>security</category>
    </item>
    <item>
      <title>Why stateless anonymization reduces exposure</title>
      <dc:creator>Veramask API Team</dc:creator>
      <pubDate>Mon, 17 Aug 2026 05:02:00 +0000</pubDate>
      <link>https://dev.to/veramask/why-stateless-anonymization-reduces-exposure-489g</link>
      <guid>https://dev.to/veramask/why-stateless-anonymization-reduces-exposure-489g</guid>
      <description>&lt;p&gt;Anonymization has to lower your risk, not move it. The moment a redaction&lt;br&gt;
service stores the text you sent it — for training, for caching, for "insights"&lt;br&gt;
— it becomes a second system holding the same regulated data you were trying to&lt;br&gt;
remove. You have not closed the boundary; you have widened it. Stateless&lt;br&gt;
anonymization is the architectural choice that prevents that drift: the service&lt;br&gt;
reads your payload in volatile memory, returns the masked version, and keeps&lt;br&gt;
nothing. The reduction in exposure is structural, not contractual.&lt;/p&gt;

&lt;p&gt;This article walks through why statelessness is the property that makes an&lt;br&gt;
anonymization tool safe to put in the critical path, where the failure mode of a&lt;br&gt;
stateful service sits, and how to reason about the trust boundary when you&lt;br&gt;
evaluate any redaction vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct answer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does stateless anonymization reduce exposure?&lt;/strong&gt; Because it removes the&lt;br&gt;
&lt;em&gt;second copy&lt;/em&gt; of the data. Every stateful redaction service — one that logs&lt;br&gt;
inputs, caches payloads, builds models, or keeps a "training set" — creates a&lt;br&gt;
new data store you now have to govern with the same retention controls, access&lt;br&gt;
policy, and breach response as the system you were trying to protect. A&lt;br&gt;
stateless service holds the data only for the duration of the request, in memory&lt;br&gt;
that is overwritten on the next call. There is no persistent copy to breach, to&lt;br&gt;
subpoena, to retroactively delete, or to forget to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the exposure actually lives
&lt;/h2&gt;

&lt;p&gt;When you send PII to a redaction service, two questions decide your residual&lt;br&gt;
risk: &lt;em&gt;how long&lt;/em&gt; the service keeps the input, and &lt;em&gt;who can reach&lt;/em&gt; what it kept.&lt;br&gt;
A surprisingly large share of "anonymization" products fail the first question&lt;br&gt;
by design.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data the service holds&lt;/th&gt;
&lt;th&gt;New exposure it creates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input payloads written to a request log&lt;/td&gt;
&lt;td&gt;A second breach surface, with the original — not anonymized — values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached payloads for "performance"&lt;/td&gt;
&lt;td&gt;A hot store governed by the cache's TTL, not your retention policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A training corpus built from customer traffic&lt;/td&gt;
&lt;td&gt;Personal data harvested for a purpose the data subject never consented to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error payloads captured for debugging&lt;/td&gt;
&lt;td&gt;Untreated PII sitting next to stack traces, often with looser access than prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repromised inputs in an analytics warehouse&lt;/td&gt;
&lt;td&gt;A copy that survives your deletion request because it lives outside your tenant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each row is a new retention clock. Under GDPR, the deletion deadline and breach&lt;br&gt;
notification timeline are reckoned from when the data was &lt;em&gt;collected&lt;/em&gt;, not from&lt;br&gt;
when you eventually delete it. A stateful redaction service that "forgets&lt;br&gt;
later" does not reset that clock — it just hands you a second one to track.&lt;/p&gt;

&lt;p&gt;The structural problem is not that stateful services are malicious. Most are&lt;br&gt;
not. The problem is that keeping data is the path of least resistance for any&lt;br&gt;
product team: logging inputs makes debugging easier, caching makes latency&lt;br&gt;
lower, and a training corpus makes the model better. Each is a reasonable&lt;br&gt;
engineering choice in isolation. Aggregated, they turn an anonymity tool into a&lt;br&gt;
PII aggregator, and the privacy posture of the &lt;em&gt;aggregator&lt;/em&gt; is now part of your&lt;br&gt;
own posture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stateless contract
&lt;/h2&gt;

&lt;p&gt;Statelessness is not a marketing claim; it is a property you can verify. A&lt;br&gt;
service is stateless with respect to your data if all of the following hold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No persistent store.&lt;/strong&gt; Input payloads are not written to disk, object
storage, a database, or an append-only log. Memory used during a request is
released at the end of the request, not pooled for reuse against the next
tenant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No input logging.&lt;/strong&gt; Request bodies are excluded from application,
access, and error logs. If an error payload must be captured for
diagnostics, the PII-bearing fields are stripped or hashed &lt;em&gt;before&lt;/em&gt; the
line is written — never after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No training on customer data.&lt;/strong&gt; Models are trained on licensed or
synthetic corpora, not on traffic flowing through the production API. This
is the one clause most easily violated silently; ask for it in writing and
check the data processing terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shared request-scoped state across tenants.&lt;/strong&gt; What one customer sends
cannot be observed by, or influence the processing of, another customer's
request. A deterministic salt supplied by the caller is the cross-request
link — not server-side session state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is the only medium.&lt;/strong&gt; Where intermediate buffers are needed (say,
to stream a large JSON payload), they live in RAM for the lifetime of the
request and are not persisted to a temp file or swap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first three are the ones that matter for compliance. The last two matter&lt;br&gt;
for correctness and multi-tenant isolation. Together, they collapse the trust&lt;br&gt;
boundary to a single request: what you send is what gets processed, and what&lt;br&gt;
gets processed is what you get back. Nothing accumulates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the cheapest privacy control you can ship
&lt;/h2&gt;

&lt;p&gt;Most privacy controls are &lt;em&gt;additive&lt;/em&gt; — they layer on top of a system that&lt;br&gt;
already holds data: encryption at rest, access controls, retention timers,&lt;br&gt;
audit logging, DPIAs. Each is a new surface you must operate and audit. Each&lt;br&gt;
reduces risk but never to zero, because the data is still &lt;em&gt;there&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Statelessness is &lt;em&gt;subtractive&lt;/em&gt;. It removes a class of risk rather than managing&lt;br&gt;
it. Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No breach surface to defend.&lt;/strong&gt; You cannot exfiltrate a database that does
not exist. The most secure data store is the one you never built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No retention clock to forget.&lt;/strong&gt; Data that is never stored cannot be
forgotten late. There is no DSAR hole, no stale backup to chase, no "we deleted
the row but the warehouse copy survived" incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No DPIA for the redaction path.&lt;/strong&gt; A stateless service that processes and
forgets does not appear as a data store in your data flow maps. You still
need a lawful basis for sending the data, but you no longer need to defend the
&lt;em&gt;vendor's&lt;/em&gt; storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No vendor-lock risk in the data.&lt;/strong&gt; A stateless service holds nothing, so
there is nothing to export, nothing to migrate, and nothing the vendor can
hold if you switch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The control is cheap because the architecture is cheap. The same property that&lt;br&gt;
protects your users protects the vendor: less state means less to operate, less&lt;br&gt;
to back up, less to audit, less to explain to regulators.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to evaluate a redaction vendor's actual surface
&lt;/h2&gt;

&lt;p&gt;When the sales deck says "we don't store your data," the architectural&lt;br&gt;
questions that disambiguate are narrow and worth asking directly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What you are really asking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Do request bodies appear in your application logs in plaintext?&lt;/td&gt;
&lt;td&gt;Whether the access log is a second breach surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How long do cached payloads live, and where?&lt;/td&gt;
&lt;td&gt;Whether "performance" extends your retention though a back door&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is customer traffic used to train your models?&lt;/td&gt;
&lt;td&gt;Whether the product improves itself on data you sent it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where do error payloads — including the raw input — get written?&lt;/td&gt;
&lt;td&gt;Whether debugging infrastructure is a quiet PII sink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is data partitioned per tenant at rest, or is it a shared store?&lt;/td&gt;
&lt;td&gt;Whether a tenant boundary breach exposes every customer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A vendor that can answer all five with "no" or "nowhere" and back it with a&lt;br&gt;
contracts clause and an audit report is stateless in the sense that matters. A&lt;br&gt;
vendor that hedges on any one — "we keep a cache for performance," "we may use&lt;br&gt;
anonymized samples to improve the model," "errors are logged with the request&lt;br&gt;
context" — is stateful by another name, and the retention clock is running on&lt;br&gt;
your data inside their boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode of a stateful redaction service
&lt;/h2&gt;

&lt;p&gt;The worst outcome is not a vendor that mishandles data; it is a vendor that was&lt;br&gt;
&lt;em&gt;designed&lt;/em&gt; to handle data safely and drifts. Stateful redaction services tend&lt;br&gt;
to add retention surfaces over time in three predictable ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Latency wins.&lt;/strong&gt; A cache for "warm" payloads gets added to reduce p99, and
suddenly inputs live for an hour at the edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality wins.&lt;/strong&gt; A model fine-tuning pipeline starts sampling production
traffic because retraining on synthetic data is not improving recall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support wins.&lt;/strong&gt; A debug capture job, scoped to one noisy tenant, starts
keeping raw request bodies "for a week, with access limited to engineering"
— and stays on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each is a reasonable local decision. None violates the original "we don't store&lt;br&gt;
your data" promise as the vendor remembers it. But the boundary you agreed to&lt;br&gt;
no longer matches the boundary that exists. A stateless architecture makes&lt;br&gt;
these drifts structurally harder to add — there is no store to extend, no&lt;br&gt;
schema to migrate, no cache tier to plumb in. The friction is not a bug; it is&lt;br&gt;
the control.&lt;/p&gt;

&lt;h2&gt;
  
  
  A mental model: shrinking the trust boundary
&lt;/h2&gt;

&lt;p&gt;You can draw any privacy architecture as a set of trust boundaries. Each&lt;br&gt;
boundary is a place where regulated data lives and a set of principals who can&lt;br&gt;
reach it. The smaller the set of boundaries that hold the original data, the&lt;br&gt;
smaller your exposure. A stateful redaction service adds a boundary — and&lt;br&gt;
extends the perimeter of any breach that hits you into that vendor's&lt;br&gt;
infrastructure.&lt;/p&gt;

&lt;p&gt;A stateless service is a &lt;em&gt;boundaryless transformer&lt;/em&gt;. It accepts data, mutates&lt;br&gt;
it, returns it. The regulated data still flows through it, but it does not&lt;br&gt;
&lt;em&gt;dwell&lt;/em&gt; in it. From a data-flow perspective, it is closer to a pure function&lt;br&gt;
than to a database. When you choose tools for the critical path — the path&lt;br&gt;
between "your user types something" and "the value reaches your log line, your&lt;br&gt;
warehouse, or a third party" — a boundaryless transformer is what you want,&lt;br&gt;
because every durable store you avoid is a store you do not have to defend.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Does "stateless" mean the service keeps nothing whatsoever?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. A stateless redaction service keeps &lt;em&gt;your data&lt;/em&gt; for no longer than the&lt;br&gt;
request. It still keeps operational metadata that any service keeps — request&lt;br&gt;
counts, latency metrics, error rates — because those are necessary to run and&lt;br&gt;
observe the service. The statelessness guarantee is specifically about the&lt;br&gt;
input payload and the masked output: neither is persisted, logged, or repromised&lt;br&gt;
beyond the lifetime of the request that produced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a stateless service still leak data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — via the operator, the network, and the in-memory window. Statelessness&lt;br&gt;
reduces the &lt;em&gt;persistent&lt;/em&gt; surface to zero, but the request still exists in&lt;br&gt;
volatile memory for some milliseconds, traverses the network, and is produced by&lt;br&gt;
code you do not control. The mitigations are the standard ones: TLS in transit,&lt;br&gt;
memory that is overwritten between requests, no shared process memory across&lt;br&gt;
tenants, and a vendor you have a real contract with. Statelessness is not the&lt;br&gt;
only control you need; it is the one that makes the &lt;em&gt;storage&lt;/em&gt; surface&lt;br&gt;
evaporate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If a stateless service forgets everything, how can it be deterministic?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Determinism comes from the output, not from remembering the input. When you&lt;br&gt;
supply a salt, the service applies a deterministic function (typically a salted&lt;br&gt;
hash) to each detected value — the same input plus the same salt always yields&lt;br&gt;
the same masked output on every call. The reproducibility lives in the&lt;br&gt;
&lt;em&gt;function&lt;/em&gt; and in the salt you keep, not in any server-side state. This is what&lt;br&gt;
lets the same value produce the same join key across log lines and services&lt;br&gt;
without the redaction service ever storing the original.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a stateful redaction service ever the right choice?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes. A large-batch job that has to replay the same corpus many times —&lt;br&gt;
for re-processing, for backfill, for multiple downstream consumers — may&lt;br&gt;
benefit from a service that holds intermediate state so callers do not&lt;br&gt;
re-upload. In those cases the right pattern is to keep the corpus &lt;em&gt;under your&lt;/em&gt;&lt;br&gt;
boundary, anonymize it in a stateless pass, and never send the raw data to a&lt;br&gt;
third party that retains it. Statefulness is acceptable inside your own&lt;br&gt;
perimeter; it is a liability when it lives in a third party's.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want a concrete implementation to compare against
&lt;/h2&gt;

&lt;p&gt;The stateless, zero-retention model described here is the architecture&lt;br&gt;
Veramask is built on: payloads are processed in volatile memory, nothing is&lt;br&gt;
persisted, and no customer traffic is used for model training. If you want to&lt;br&gt;
see how that contract is expressed in the API surface — entity types,&lt;br&gt;
override strategies, the consistency-salt mechanism that makes output&lt;br&gt;
deterministic without server-side state — the&lt;br&gt;
&lt;a href="https://veramask.com/apidoc" rel="noopener noreferrer"&gt;architecture and API docs&lt;/a&gt; walk through it end to end.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>gdpr</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Choosing a PII anonymization strategy: when mask, redact, replace, hash, or substitute wins</title>
      <dc:creator>Veramask API Team</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/veramask/choosing-a-pii-anonymization-strategy-when-mask-redact-replace-hash-or-substitute-wins-54e9</link>
      <guid>https://dev.to/veramask/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>Veramask API Team</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:55:11 +0000</pubDate>
      <link>https://dev.to/veramask/the-hidden-cost-of-logging-pii-5f0l</link>
      <guid>https://dev.to/veramask/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>Veramask API Team</dc:creator>
      <pubDate>Thu, 09 Jul 2026 21:48:22 +0000</pubDate>
      <link>https://dev.to/veramask/how-to-anonymize-pii-in-text-with-an-api-1p69</link>
      <guid>https://dev.to/veramask/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>
