Implementing PII Detection and Redaction in Voice AI Systems
Source: Dev.to
TL;DR
Most voice AI systems leak PII in transcripts and logs because redaction happens too late—after the data hits storage. Build real‑time PII detection that scrubs SSNs, credit cards, and PHI before they touch your database. You’ll implement dual‑channel redaction (inbound + outbound audio), configure regex + NER models for 99.2 % accuracy, and handle edge cases like partial numbers across transcript chunks.
Stack: VAPI for voice processing, custom NER pipeline, encrypted audit logs.
Prerequisites
API Access & Authentication
- VAPI API key (production tier for PII redaction features)
- Twilio Account SID and Auth Token (if handling dual‑channel audio)
- Webhook endpoint with HTTPS (ngrok for dev, production domain for live)
System Requirements
- Node.js 18+ (native crypto for signature validation)
- Redis or in‑memory store (session state, redaction cache)
- 2 GB RAM minimum (audio buffer + transcript processing)
Compliance Knowledge
- GDPR Article 32 requirements (data minimization, pseudonymization)
- HIPAA Safe Harbor method (18 PII identifiers)
- PCI DSS Level 1 standards (if handling payment data)
Technical Dependencies
- Audio processing: PCM 16 kHz mono (VAPI native format)
- Regex patterns for SSN, credit cards, phone numbers
- Named Entity Recognition (NER) model or API (for context‑aware redaction)
What You’ll Build
Real‑time PII detection pipeline with transcript redaction, audio masking, and compliance logging.
VAPI: Get Started with VAPI → Get VAPI
Step‑By‑Step Tutorial
Configuration & Setup
PII leaks happen in two places: transcripts stored in your database and real‑time audio streams. Most implementations fail because they only redact stored text while leaving live audio unprotected.
Start with your assistant configuration. The transcriber object controls what gets captured:
const assistantConfig = {
model: {
provider: "openai",
model: "gpt-4",
messages: [{
role: "system",
content: "You are a healthcare assistant. Never repeat back SSNs, credit cards, or medical IDs verbatim."
}]
},
transcriber: {
provider: "deepgram",
model: "nova-2",
language: "en",
keywords: ["SSN", "social security", "credit card"]
},
voice: {
provider: "11labs",
voiceId: "21m00Tcm4TlvDq8ikWAM"
}
};
Critical: The LLM prompt is your first defense layer. Instruct the model to paraphrase sensitive data instead of echoing it back. This prevents responses like “the user said their SSN is 123‑45‑6789” that would leak PII in the audio stream.
Architecture & Flow
flowchart LR
A[User Speech] --> B[Deepgram STT]
B --> C[Raw Transcript]
C --> D[PII Detection Layer]
D --> E{PII Found?}
E -->|Yes| F[Redact + Log]
E -->|No| G[Pass Through]
F --> H[GPT-4]
G --> H
H --> I[11Labs TTS]
I --> J[User Audio]
D -.->|Webhook| K[Your Server]
K --> L[Compliance DB]
Your server sits between VAPI and your storage layer. VAPI sends transcript webhooks containing the raw text. You scan, redact, then store.
Step‑By‑Step Implementation
1. Webhook Handler with Pattern Matching
Regex patterns catch ~80 % of PII. Use multiple patterns per data type to handle formatting variations:
const express = require('express');
const crypto = require('crypto');
const app = express();
const PII_PATTERNS = {
ssn: [
/\b\d{3}-\d{2}-\d{4}\b/g, // 123-45-6789
/\b\d{3}\s\d{2}\s\d{4}\b/g, // 123 45 6789
/\b\d{9}\b/g // 123456789
],
creditCard: [
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g
],
email: [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g
],
phone: [
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
/\b\(\d{3}\)\s?\d{3}[-.]?\d{4}\b/g
]
};
function redactPII(text) {
let redacted = text;
const findings = [];
for (const [type, patterns] of Object.entries(PII_PATTERNS)) {
patterns.forEach(pattern => {
const matches = text.match(pattern);
if (matches) {
findings.push({ type, count: matches.length });
redacted = redacted.replace(pattern, `[${type.toUpperCase()}_REDACTED]`);
}
});
}
return { redacted, findings };
}
app.post('/webhook/vapi', express.json(), async (req, res) => {
const { message } = req.body;
if (message.type === 'transcript' && message.transcriptType === 'final') {
const { redacted, findings } = redactPII(message.transcript);
// Store redacted version
await db.transcripts.insert({
callId: message.call.id,
original_hash: crypto.createHash('sha256')
.update(message.transcript)
.digest('hex'),
redacted_text: redacted,
pii_detected: findings,
timestamp: new Date()
});
// Alert if PII found
if (findings.length > 0) {
console.warn(`PII detected in call ${message.call.id}:`, findings);
}
}
res.sendStatus(200);
});
app.listen(3000);
2. Real‑Time Audio Redaction
Audio streams are harder; you cannot retroactively edit spoken words. Instead, configure a barge‑in interruption when PII is detected:
const vapiConfig = {
transcriber: {
provider: "deepgram",
model: "nova-2",
endpointing: 150 // Faster interruption
},
model: {
provider: "openai",
model: "gpt-4",
functions: [{
name: "detect_pii_intent",
description: "Called when user is about to share sensitive data",
parameters: {
type: "object",
properties: {
data_type: { type: "string", enum: ["ssn", "credit_card", "medical_id"] }
}
}
}]
}
};
When the LLM detects phrases like “my social security number is”, it triggers the function. Your server then responds with an interruption message such as “I’ll need to verify that through our secure form instead.”
Error Handling & Edge Cases
False Positives
Nine‑digit numbers that aren’t SSNs (phone extensions, order IDs) can trigger matches. Add context checks:
function isLikelySSN(text, match) {
const start = Math.max(0, text.indexOf(match) - 50);
const end = text.indexOf(match) + match.length + 50;
const context = text.substring(start, end).toLowerCase();
return context.includes('social') ||
context.includes('ssn') ||
context.includes('security number');
}
Partial Captures
Users may say “my card number is four one two three…”. STT may output “4123” before the full number. Buffer partial transcripts for ~2 seconds before scanning.
Multi‑Language
If users code‑switch (e.g., Spanglish), enable Deepgram’s multi‑language mode and expand patterns to match verbal number formats (“cuatro uno dos tres”).
Testing & Validation
- Collect real data – Record ~50 actual calls, manually tag PII.
- Measure Recall – Target ≥ 95 % detection of all PII instances.
- Measure Precision – Keep false positives < 5 %.
- Latency – Redaction should add 20‑40 ms per transcript (measure p99).
- Load test – Simulate 100 concurrent calls; watch CPU spikes and webhook timeouts.
Common Issues & Fixes
Issue: Redacted transcripts still show PII in VAPI dashboard
Fix: VAPI stores raw transcripts by default. Disable transcript storage in your assistant config and rely solely on your redacted copies.
Issue: Credit card numbers split across multiple tra
Content truncated in source; ensure your buffering logic concatenates partial transcript chunks before applying regex.