Conversational Privacy

Real-Time PII Filtering for Chatbots

Chat is where customers volunteer their most sensitive data — card numbers, addresses, health details — one message at a time. Learn how to mask PII live in bots, live chat, and agent-assist tools, keep transcripts compliant, and do it inside a websocket round-trip.

Explore the Guide

Why Chat Is a PII Magnet

Chat interfaces train users to over-share. The conversational register feels private and ephemeral — like talking, not like filling in a form — so customers type things into a chat widget they would never put in an email: full card numbers "so you can just fix it," a photo of a driver's license, a child's date of birth, medication names, one-time passcodes. Unlike a form, chat has no field validation to refuse the input. Whatever is typed becomes part of the record.

And chat records multiply aggressively. A single customer message typically lands in the chat vendor's database, your own conversation store, the bot platform's logs, the LLM provider's request logs if a model powers the bot, the analytics warehouse that computes deflection rates, the QA tool that samples conversations for coaching, and the CRM timeline synced for the account team. One pasted card number becomes seven stored copies in seven systems, most of them outside any diagram your compliance team has seen.

The regulatory exposure follows the copies. GDPR and CCPA treat every one of those stores as processing that needs a basis, a retention period, and coverage in access and deletion requests. PCI DSS drags any system storing a card number into audit scope. HIPAA turns a symptom description in a patient-support chat into PHI. Meanwhile the conversational-AI wave means bot completions themselves can emit PII pulled from context or connected tools.

The fix is architectural, not behavioral: intercept every message at the moment it crosses a boundary, detect sensitive entities with our PII Detection API, and decide — mask, block, or flag — before the message is displayed, stored, or forwarded. Done at the right points, filtering is invisible to users, protective for agents, and it turns seven risky copies into seven masked ones.

Where PII Enters a Conversation

Four distinct actors put PII into chat, and each needs slightly different handling. Filtering only customer messages — the common first implementation — leaves three doors open.

Customer Messages

The highest-volume source: identifiers volunteered for convenience ("my email is..."), payment data pasted in frustration, and sensitive context shared to explain a problem. Detection here should be broad — all entity types, moderate threshold — with masking applied before the message reaches storage, the bot's LLM, or an agent's screen, depending on your policy per entity type.

Bot and LLM Completions

A bot wired to order systems, CRMs, or a retrieval index can surface another customer's data through a lookup bug or a model's confusion — the classic wrong-recipient disclosure. Completions need a post-generation scan before display, with a hard block on anything that looks like credentials or payment data. Our LLM guardrails guide covers the model side in depth.

Agent Replies and Internal Notes

Agents copy account details into replies to be helpful, and internal notes ("cust SSN ends 2233, verified") accumulate identifiers that outlive the conversation. Agent-assist filtering works best as a nudge: detect on send, show the agent what will be masked, and let overrides require a click plus a reason. Notes should be scanned exactly like customer-visible text — auditors read them too.

Attachments and Transcribed Media

Screenshots of statements, photos of ID documents, forwarded emails, and — in voice-enabled channels — speech-to-text transcripts. Run OCR or transcription first, then push the extracted text through the same detection pipeline as typed messages. A conversation store is only as clean as its least-filtered input path.

Filtering Architecture: Five Interception Points

A chat message's lifecycle offers five places to put a detection call, and mature deployments use most of them with different policies. (1) On receive — the message arrives at your chat backend over websocket or webhook, before any processing. This is the master checkpoint: scan once, attach the entity metadata to the message object, and let every downstream consumer reuse the result instead of re-scanning. (2) Before the bot/LLM — the assembled prompt (message plus history plus retrieved context) gets a policy check so third-party model providers never see raw identifiers. (3) Before display — bot completions and cross-agent views are scanned so the UI never renders unvetted text.

(4) Before storage — the persistence layer writes the masked text plus entity metadata; the raw value, if operationally needed (say, a phone number the agent must actually dial), goes to a separate vault with its own access control and TTL rather than living in the transcript. (5) Before export — the syncs to analytics, CRM, QA tools, and data lakes carry masked text only. Points four and five are where compliance is won: they determine what exists a year later when the access request or the breach happens.

Policy should vary by entity type, not by channel. A workable default: hard-mask payment data (CREDIT_CARD_NUMBER, CVV_NUMBER), government IDs (SSN, PASSPORT_NUMBER), and credentials (PASSWORD, API_KEY) everywhere including the agent view; soft-mask contact identifiers (EMAIL_ADDRESS, PHONE_NUMBER, ADDRESS) in storage and analytics while showing agents the live value during the session; and flag-only for PERSON_NAME in channels where names are operationally necessary.

Scan once, reuse everywhere. Because the API returns character offsets, one scan at the receive point yields both the masked rendering and the exact positions for any consumer that needs different treatment. Five interception points does not mean five API calls per message — typically it means one or two.

Websocket and Streaming Patterns

Chat is latency-sensitive but message-oriented, which makes it friendlier to filtering than it first appears. A detection call runs 100–300 ms — imperceptible against typing cadence. Streaming bot output is the only genuinely hard case, and three patterns cover it.

Per-Message Gate (Default)

For human messages, scan the complete message in the websocket handler before fan-out: receive, detect, mask, then broadcast and store. Users perceive nothing — the round-trip fits inside normal network jitter. Use connection pooling / keep-alive to the detection endpoint and a hard client timeout (2–5 s) with an explicit fail-open or fail-closed decision per deployment.

  • One API call per message; batch history imports separately
  • Attach detected_entities metadata to the message object
  • Fail closed for regulated channels (payments, health)

Sentence-Chunk Streaming

For token-streamed bot replies, buffer tokens until a sentence boundary closes, scan the sentence, release it masked, and continue. Perceived latency shifts by one sentence — usually 200–400 ms — while guaranteeing no unscanned text ever renders. This is the recommended pattern for regulated deployments because it is both streaming and strictly safe.

  • Buffer at sentence or clause boundaries, not token counts
  • Entities never straddle a completed sentence in practice
  • Overlap: scan sentence N while the model generates N+1

Optimistic Stream with Retraction

Where every millisecond of streaming matters and risk tolerance allows, render tokens immediately while a trailing scan runs over the accumulating text; on a hit, send a websocket edit event that replaces the rendered message with the masked version. Exposure is real but brief (under a second) and client-side only. Never use this pattern for payment or health channels — brief display is still display.

  • Requires an editable-message protocol on the client
  • Store only the final masked version, never the optimistic one
  • Log every retraction as a near-miss metric

Masking Before Storage and Analytics

The conversations your product team mines for intents, your QA team samples for coaching, and your data team joins against orders are the same conversations your regulators treat as personal-data stores. Storing masked text changes their status fundamentally: a transcript reading "my card is [CREDIT_CARD_NUMBER] and I live at [ADDRESS]" trains intent models, computes CSAT drivers, and answers "what did the customer ask" exactly as well as the raw version — while dropping out of PCI scope and shrinking every privacy request that touches it.

The mask_mode choice per destination matters. Use replace for QA and CRM copies, where readable placeholders keep context. Use hash for the analytics warehouse: each unique value maps to a stable token, so "how many conversations did this customer have" and funnel joins keep working without any store holding the actual email address. Use redact for exports leaving your organization — training corpora for vendors, benchmarking datasets — where even placeholder structure is unnecessary.

A single call does the work at write time:

curl -X POST https://piidetectionapi.com/api/moderate.php \ -H "Content-Type: application/json" \ -d '{ "api_key": "YOUR_API_KEY", "api_type": "pii_detection", "text": "sure my number is 415-555-0182 and card 4111 1111 1111 1111, im at 88 Pine St Apt 4B, Seattle", "mask_mode": "hash", "threshold": 0.5, "custom_instruction": "Do not mask order numbers of the form ORD-XXXXXX" }'

Note the custom_instruction: chat text is full of business tokens (order IDs, RMA numbers, tracking codes) that look identifier-like. A one-line natural-language exclusion keeps them intact so downstream automation that parses them never breaks. The response's entities_detected count per conversation also becomes a useful analytics dimension in its own right — which flows, bots, and prompts cause customers to over-share.

Transcript Compliance by Regulation

Chat transcripts sit at the intersection of most data-protection regimes. This table summarizes what each expects of stored conversations and where real-time detection carries the load.

Regulation What it means for transcripts Detection's role Key entity types
GDPR Transcripts are personal data: lawful basis, minimization, retention limits, and erasure/access rights apply to every stored copy Mask identifiers at write time so stored transcripts are minimized by default; locate a data subject's messages for requests PERSON_NAME, EMAIL_ADDRESS, PHONE_NUMBER, ADDRESS, IP_ADDRESS
CCPA/CPRA Transcripts count in access, deletion, and correction requests; SPI shared in chat triggers limitation rights Classify entities per conversation so requests can be fulfilled; strip SPI before analytics use SSN, GPS_COORDINATES, RELIGION, MEDICAL_DATA
PCI DSS A stored PAN puts the chat platform in scope; a stored CVV is a violation outright Inline masking keeps card data out of transcripts entirely — the descoping control CREDIT_CARD_NUMBER, CVV_NUMBER, CREDIT_CARD_EXPIRATION_DATE
HIPAA Health details in patient-facing chat are PHI; storage and vendor access require BAAs and safeguards Detect and mask the 18 identifiers plus medical context before storage or vendor sync MEDICAL_RECORD_NUMBER, DIAGNOSIS, PRESCRIPTION, DATE_OF_BIRTH
Call-recording / wiretap laws (e.g. CIPA) Chat session recording and monitoring tools face consent requirements; minimizing captured PII reduces exposure Filter session-replay and monitoring feeds the same way as transcripts PERSON_NAME, EMAIL_ADDRESS, payment entities

The pattern across every row: the regulation is about stored conversations, and the control is applied at the moment of storage. Retention policies then get simpler too — a masked transcript can often be kept for the full business-useful period, because what made retention risky has already been removed.

Implementation: A Filtering Websocket Layer

Two production-shaped examples: a Node.js websocket handler implementing the per-message gate with per-entity policy, and a Python sentence-chunk filter for streaming bot replies. Both call the canonical endpoint documented in the API documentation.

JavaScript (Node) — websocket message gate

const HARD_MASK = new Set(["CREDIT_CARD_NUMBER", "CVV_NUMBER", "SSN", "PASSWORD", "PASSPORT_NUMBER", "API_KEY"]); async function detect(text) { const res = await fetch("https://piidetectionapi.com/api/moderate.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: process.env.PII_API_KEY, api_type: "pii_detection", text, mask_mode: "replace", threshold: 0.5 }), signal: AbortSignal.timeout(4000) // bounded wait }); return res.json(); } wss.on("connection", (socket) => { socket.on("message", async (raw) => { const msg = JSON.parse(raw); let scan; try { scan = await detect(msg.text); } catch (err) { // Regulated channel: fail closed rather than leak socket.send(JSON.stringify({ type: "retry", id: msg.id })); return; } const hardHits = scan.detected_entities .filter(e => HARD_MASK.has(e.type)); // Store + broadcast the masked text; agents see soft entities live await store.save({ id: msg.id, text: scan.anonymized_text, // masked at rest entities: scan.detected_entities.map(({type, start, end, confidence}) => ({type, start, end, confidence})), // metadata, no raw values }); broadcast(room(msg), { ...msg, text: hardHits.length ? scan.anonymized_text : msg.text, piiTypes: scan.detected_entities.map(e => e.type) }); }); });

Python — sentence-chunk filter for a streaming bot reply

import re, requests API = "https://piidetectionapi.com/api/moderate.php" SENTENCE_END = re.compile(r"(?<=[.!?])\s+") def mask(text): r = requests.post(API, json={ "api_key": "YOUR_API_KEY", "api_type": "pii_detection", "text": text, "mask_mode": "replace", "threshold": 0.5, }, timeout=5) r.raise_for_status() return r.json()["anonymized_text"] async def stream_filtered(token_stream, send): """Buffer model tokens to sentence boundaries; release masked.""" buf = "" async for token in token_stream: buf += token parts = SENTENCE_END.split(buf) # Everything except the trailing fragment is complete for sentence in parts[:-1]: await send(mask(sentence) + " ") buf = parts[-1] if buf.strip(): await send(mask(buf)) # flush the last fragment

In production, run the mask() call for sentence N concurrently with generation of sentence N+1 (an asyncio task queue) and the added latency effectively disappears. For an end-to-end view of guarding the model call itself — prompt side as well as completion side — see the LLM guardrails guide; for volumes and rate limits, the pricing page lists chat-scale tiers.

Best Practices for Chat PII Filtering

Operational lessons from chat platforms filtering millions of messages a day.

Tell the User What Happened

Silent masking confuses customers ("why does my message say [CREDIT_CARD_NUMBER]?"). Pair the filter with a gentle system message: "For your security, we've hidden the card number you shared — our agents never need the full number." This turns the control into visible trust-building, reduces re-pasting, and trains users toward safer behavior. Bots should follow up by offering the safe path, such as a PCI-compliant payment link.

Exclude Business Tokens Explicitly

Chat is dense with order numbers, ticket IDs, SKUs, and confirmation codes that pattern-based systems mangle. Use the custom_instruction parameter to describe your token formats once ("do not mask order numbers like ORD-123456 or tracking numbers starting 1Z"), and keep a per-channel exclusion config under version control. Every false positive an agent sees erodes trust in the filter; a tight exclusion list keeps override rates near zero.

Monitor the Filter Like a Feature

Track detections per thousand messages by entity type, channel, and bot flow; track scan latency percentiles against your websocket budget; track agent overrides and retractions as quality signals. Spikes are information: a jump in card-number detections after a checkout change means the product is pushing users to paste PANs into chat. Review a weekly sample of masked messages for false positives and feed fixes into thresholds and exclusions.

Frequently Asked Questions

Will filtering add noticeable lag to conversations?
No. A detection call completes in 100–300 ms, comfortably inside the natural rhythm of chat — users pause longer than that between reading and replying. For human messages the scan runs before fan-out and is imperceptible; for streaming bot replies, sentence-chunk filtering shifts perceived latency by one sentence at most, and overlapping scans with generation hides even that. The processing_time_ms field in every response lets you monitor real latency against your budget continuously.
Should agents see the real values or the masked ones?
Split by entity type. Agents genuinely need contact details during a live session, so soft entities (phone, email, address) can display live while being masked in storage. Agents almost never need full card numbers, SSNs, or passwords — payment should happen through a compliant link, identity verification through last-four confirmation — so hard entities stay masked even in the agent view. This "clean-room agent desktop" also protects agents themselves from becoming an insider-risk vector and simplifies your PCI attestation for the contact center.
What happens when a customer intentionally needs to share sensitive data, like updating a card?
Route around the transcript, not through it. The bot or agent triggers a secure side-channel — a hosted payment field, an identity-verification flow, a document-upload portal with its own controls — and the chat records only that the step happened. The filter then correctly masks anything the customer pastes into chat anyway, which they will. This is the same pattern contact centers use for voice ("pause recording"), implemented as a product flow instead of a manual toggle.
Does masking break chatbot NLU and intent analytics?
Intent models care about linguistic structure, not identifier values — "I want a refund to [EMAIL_ADDRESS]" classifies identically to the raw version. In practice masked corpora often improve NLU training by preventing models from overfitting to specific customer values. For analytics that need to count or join on identity, hash mode preserves stable per-value tokens so distinct-customer counts and session joins keep working. The one thing to protect is business tokens (order IDs), which the custom_instruction exclusion handles.
Can this work with third-party chat platforms we don't control?
Usually yes, at one of three points: most platforms support middleware or webhook hooks that fire on message events before storage or routing; if not, place the filter in your bot backend (the piece you always control) so at least bot-visible and LLM-bound text is clean; and at minimum, filter the export/sync jobs that copy transcripts into your CRM, warehouse, and QA tools. Full in-platform masking depends on the vendor's hook model, but the storage and analytics copies — where most regulatory exposure lives — are always reachable.
How do we handle multilingual chats?
The same request works across 60+ languages with no language parameter — the models detect entities in French, German, Spanish, Japanese, Arabic, and mixed-language messages in a single pass, which matters because customers switch languages mid-conversation more often than teams expect. When calibrating thresholds, include samples from every language your channels serve; names in particular benefit from context-aware detection in languages where regex-era tooling never worked at all. The supported-languages page lists current coverage.

Make Every Conversation Safe to Store

Mask card numbers, identifiers, and health details in real time — in bots, live chat, and agent tools — with one API call per message. Paste a sample chat into the demo and watch it work.

Try the Live Demo View Pricing