Customers volunteer everything to support: card numbers, ID scans, medical details, passwords. This guide shows how to detect and mask that data in Zendesk, Intercom, and Freshdesk — with webhook-based scanning code, agent-visible masking that keeps tickets workable, and analytics that stay useful after scrubbing.
Explore the GuideSupport is the one channel where customers are actively trying to hand you sensitive data. A user who wants a refund pastes the full card number "to make it faster". A patient disputing an invoice describes their diagnosis. Someone locked out of their account sends a photo of their passport — unprompted — because it worked with their bank. Unlike a checkout form, which collects exactly the fields it is designed to collect, a ticket is a free-text box connected directly to a customer's sense of urgency. Over-sharing is the norm, not the exception.
The result is that helpdesks quietly become one of the largest unmanaged PII stores in the company. Ticket bodies, comment threads, chat transcripts, satisfaction-survey verbatims, and attachments accumulate for years, indexed and searchable by every agent, team lead, and — via analytics exports and BI connectors — by teams far outside support. Agents also copy ticket content outward: into Slack threads when they escalate, into Jira when they file bugs, into macros and knowledge-base drafts. Every copy inherits the pasted card number.
The exposure is broader than most audits assume. Helpdesk data routinely flows to sub-processors (translation services, AI summarizers, QA scoring tools), to offshore BPO partners with wide read access, and into training corpora for support chatbots. A single ticket containing a name, address, and payment card can therefore surface in half a dozen systems, none of which appear in the data map as "stores cardholder data". PCI DSS assessors increasingly ask about helpdesks for exactly this reason, and GDPR's data-minimization principle applies to a Zendesk instance just as it does to a database.
The fix is not telling customers to stop over-sharing — they won't — but detecting sensitive data the moment it arrives and masking it before it spreads. Because the PII Detection API returns typed entities with offsets, confidence scores, and a ready-masked version of the text, the entire workflow — scan on ticket creation, redact in place, tag for audit — fits in a small webhook handler, which this guide builds step by step. You can preview detection behavior on a sample ticket right now in the live demo.
Ticket scanning has a property most channels lack: some PII is supposed to be there. The requester's name and email are the ticket's addressing metadata; masking them everywhere would break support itself. Good policy distinguishes three classes. Never-store data — full payment card numbers, CVVs, passwords, government ID numbers, one-time codes — should be masked unconditionally, at full strength, the moment they are detected. There is no support workflow that legitimately needs a CVV in a ticket body.
Workflow data — the requester's own name and contact details in the ticket header — should generally be left intact in the working view but excluded from exports and analytics. This is where the API's scoping parameters earn their keep: pass entities to target only the high-risk types on ingest, and use custom_instruction — for example, "do not flag the requester name Maria Novak or the support address [email protected]" — to stop the ticket's own addressing data from generating noise.
Contextual data — third-party names, addresses, dates of birth, health details, order and account identifiers — sits in between: often necessary to resolve the case, rarely necessary to keep after resolution. The pragmatic policy is time-based: leave contextual entities visible while the ticket is open, then run a scrubbing pass at closure (or after a fixed grace period) that masks them before long-term retention. Detection confidence supports this nuance too: high-confidence card numbers get auto-redacted; medium-confidence hits can simply add a warning tag an agent can review.
CREDIT_CARD_NUMBER, CVV_NUMBER, SSN, NATIONAL_ID, PASSPORT_NUMBER, PASSWORD, AUTH_TOKEN, IBAN_CODE; at-closure — PERSON_NAME, ADDRESS, PHONE_NUMBER, DATE_OF_BIRTH, MEDICAL_DATA. The full catalog of 150+ types is on the entities page.
All three major helpdesks expose the same three hooks — an outbound event (webhook), a REST API to write redacted content back, and native redaction endpoints for permanent removal. The pattern is identical; only the endpoint names change.
Register a webhook fired by a trigger on "ticket created / comment added" that posts the comment body to your scanner. On detection, call the Ticket Comment Redaction API (or comment_redactions for permanent removal from history and attachments-in-comments), then tag the ticket (e.g. pii_redacted) so views and SLAs can track it. For agent-composed replies, the same trigger on outbound comments catches PII agents paste back to customers.
Subscribe to conversation.user.created and conversation.user.replied webhook topics. Scan each conversation part as it arrives; because Intercom is chat-first, latency matters — scan asynchronously and use the Conversations API to edit or note the offending part, and conversation attributes to record which entity types appeared. The same scanner protects Fin/bot training exports, closing the loop with your chatbot PII filtering.
Use automation rules ("ticket created" / "note added") to fire a webhook with the ticket body, scan, then PUT the masked description back via the Tickets API and add a private note summarizing what was masked. Freshdesk's marketplace app framework can also render an in-agent widget showing detected entities with an unmask-on-demand control backed by your access policy.
Running your own support stack, or Help Scout, Front, Salesforce Service Cloud? The pattern holds: intercept message-created events, scan, persist the masked body, store the raw only if a policy requires it and then encrypted with restricted access. In a custom stack you can go one step further and scan before first persistence, so the raw value never touches the database at all.
Where to hook scanning, how to write the redaction back, and what each platform offers for permanent removal.
| Capability | Zendesk | Intercom | Freshdesk |
|---|---|---|---|
| Scan trigger | Trigger + webhook on ticket/comment events | Webhook topics (conversation created/replied) | Automation rule firing a webhook |
| Write-back for masking | Comment Redaction API; ticket field updates | Conversations API (edit part, add note) | Tickets API (PUT description), private notes |
| Permanent removal | Yes — redaction endpoints strip content from history | Contact/conversation deletion; part edit | Yes — via ticket update; attachment delete API |
| Tagging for audit | Ticket tags + custom fields | Conversation attributes + tags | Tags + custom ticket fields |
| Attachment access | Attachment URLs on comments | Attachment URLs on parts | Attachment URLs on tickets/notes |
| Typical scan latency budget | Seconds (async, before broad agent access) | Sub-second preferred (live chat), async fallback | Seconds (async on create/update) |
Start with the raw detection call your webhook will make. A typical over-sharing ticket looks like this — note the mixture of legitimate workflow data and never-store data:
# Scan a new ticket body for high-risk entities
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": "Please refund my order. Card: 4111 1111 1111 1234, exp 04/27, CVV 812. Im Anna Schmidt, Hauptstrasse 12, Berlin. You can also reach me at +49 170 555 0117.",
"entities": ["CREDIT_CARD_NUMBER", "CREDIT_CARD_EXPIRATION_DATE", "CVV_NUMBER",
"SSN", "NATIONAL_ID", "PASSPORT_NUMBER", "PASSWORD", "IBAN_CODE",
"PERSON_NAME", "ADDRESS", "PHONE_NUMBER"],
"mask_mode": "replace",
"threshold": 0.6,
"custom_instruction": "Do not flag order numbers or ticket reference codes."
}'The response identifies each entity with offsets and confidence, and anonymized_text arrives ready to write back. The Node.js handler below is a complete Zendesk-style webhook receiver: it scans the comment, redacts via the helpdesk API only when high-risk types appear, and tags the ticket for auditability. The same skeleton serves Intercom and Freshdesk with different write-back URLs:
// Express webhook: scan new comments, redact high-risk PII, tag ticket
const HIGH_RISK = ["CREDIT_CARD_NUMBER", "CVV_NUMBER", "SSN",
"NATIONAL_ID", "PASSPORT_NUMBER", "PASSWORD", "IBAN_CODE"];
app.post("/hooks/ticket-comment", async (req, res) => {
res.sendStatus(200); // ack fast; work async
const { ticket_id, comment_id, body } = req.body;
const scan = 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: body,
entities: [...HIGH_RISK, "PERSON_NAME", "ADDRESS", "PHONE_NUMBER"],
mask_mode: "replace",
threshold: 0.6
})
}).then(r => r.json());
const risky = scan.detected_entities
.filter(e => HIGH_RISK.includes(e.type) && e.confidence >= 0.8);
if (!risky.length) return;
// 1) Replace the comment with the masked rendering
await zendesk.put(
`/api/v2/tickets/${ticket_id}/comments/${comment_id}/redact`,
{ text: scan.anonymized_text });
// 2) Tag + audit trail (types and counts only — never raw values)
await zendesk.put(`/api/v2/tickets/${ticket_id}`, {
ticket: { additional_tags: ["pii_redacted"],
comment: { public: false,
body: `Auto-redacted: ${risky.map(e => e.type).join(", ")}` } }
});
});Two operational details matter. Acknowledge the webhook immediately and do the scan-and-redact asynchronously — helpdesks retry slow webhooks and you do not want duplicate redactions. And make the handler idempotent: record scanned comment IDs so a redelivered event is a no-op rather than a second private note.
Full redaction is right for card data, but masking everything an agent might need turns support into archaeology. The middle path is agent-visible masking: the working view shows typed placeholders — "customer at [ADDRESS] reports her card [CREDIT_CARD_NUMBER] was charged twice" — which preserve the story of the ticket while removing the values. Typed placeholders matter: an agent who sees [CREDIT_CARD_NUMBER] knows the customer already sent a card number and can respond appropriately ("we've removed your card details for your security") instead of asking the customer to repeat it.
Where a workflow genuinely needs a value — verifying the last four digits, calling the customer back — implement unmask-on-demand: the raw value (or better, only the fragment needed) is stored encrypted and fetched through an access-controlled endpoint that logs who unmasked what and why. In practice fewer than 2% of masked values ever get unmasked, which tells you how little of the raw data support work actually requires. The Python service below produces both renderings in one API call — masked body for the ticket, and a structured entity map for the vault:
import requests
def process_ticket_body(body):
"""Returns (masked_body, entity_map) for agent view + secure vault."""
resp = requests.post(
"https://piidetectionapi.com/api/moderate.php",
json={
"api_key": "YOUR_API_KEY",
"api_type": "pii_detection",
"text": body,
"entities": ["CREDIT_CARD_NUMBER", "CVV_NUMBER", "SSN",
"PERSON_NAME", "ADDRESS", "PHONE_NUMBER",
"DATE_OF_BIRTH", "MEDICAL_DATA"],
"mask_mode": "replace",
"threshold": 0.6,
},
timeout=30,
)
data = resp.json()
entity_map = []
for i, e in enumerate(data["detected_entities"]):
entity_map.append({
"ref": f"ent_{i}",
"type": e["type"],
"start": e["start"],
"end": e["end"],
"confidence": e["confidence"],
# store e["text"] ONLY in the encrypted vault, keyed by ref
})
return data["anonymized_text"], entity_map
masked, ents = process_ticket_body(
"Charge on my card 4111 1111 1111 1234 - Anna Schmidt, DOB 03/03/1987")
print(masked)
# Charge on my card [CREDIT_CARD_NUMBER] - [NAME], DOB [DATE_OF_BIRTH]For deterministic cross-ticket correlation — "how many tickets did this customer open?" without exposing identity to analysts — run a second pass with mask_mode: "hash". The same email address always yields the same hash, so joins keep working on scrubbed data. This is the bridge to the analytics section below.
The historical objection to redacting tickets was "we need the data for analytics". It turns out almost nothing support analytics needs is the PII itself. Topic modeling, CSAT driver analysis, deflection measurement, agent QA, escalation prediction — all operate on what the ticket is about, not who the customer is. Masked text with typed placeholders preserves the aboutness completely: "[NAME] cannot log in after the [DATE] release" clusters exactly as well as the raw sentence, and arguably better, since names and numbers are noise to a topic model anyway.
A scrubbing pass therefore belongs at the boundary between the helpdesk and every downstream consumer: the nightly export to the warehouse, the BI connector, the training-set builder for your support LLM, the sample sets sent to QA vendors. Feed them anonymized_text plus the entity metadata, and three new analytics become possible that raw data never offered: PII inflow rate (which channels and forms cause customers to over-share — a web form with a free-text box next to a payment page will light up), agent hygiene (which teams paste sensitive data into escalations), and compliance posture (tickets containing card data per week, trending to zero as your prevention improves).
Where identity-level analysis is legitimately needed — churn-risk joins, lifetime-value segmentation — hash mode provides pseudonymous keys that join across tickets and even across systems without exposing the underlying identity to the analyst. Access to the mapping stays with the small team that operates the vault. This layered design typically satisfies both the analytics team and the DPO, a combination rarely achieved with raw exports; the regulatory reasoning is laid out in the GDPR PII detection guide.
Three regimes dominate helpdesk compliance. PCI DSS is the sharpest: a full PAN in a ticket makes the helpdesk part of the cardholder data environment, and a stored CVV is a violation outright — no compensating control permits it. Automated detection and immediate redaction is the recognized way to keep a helpdesk out of PCI scope, and assessors will ask to see it working. GDPR/CCPA apply data minimization, purpose limitation, and subject rights: a five-year-old resolved ticket containing an address and date of birth is stored personal data with no remaining purpose, and it is discoverable by an access request — meaning your DSAR process must search tickets too. Scrub-at-closure plus finite retention shrinks both the risk and the DSAR workload dramatically.
HIPAA reaches any helpdesk where patients discuss care — telehealth support, insurance member services, provider IT desks. PHI in tickets requires access controls, audit trails, and business-associate agreements with the helpdesk vendor and every connected tool. Detection with a health-scoped entity list (MEDICAL_RECORD_NUMBER, DIAGNOSIS, HEALTH_INSURANCE_ID, PRESCRIPTION) lets you find PHI that arrived where it should not, and the HIPAA PHI detection guide maps entity types to the 18 identifiers.
Operationally, encode retention as a ladder the scanner enforces: on ingest, mask never-store data; at closure, mask contextual PII; at retention horizon (say 24 months), either delete or reduce the ticket to its scrubbed analytical skeleton. Each rung is a scheduled job over the helpdesk API using the same detection call, and the ticket tags written at each stage are your audit evidence. Teams running high-volume desks batch the closure pass nightly; API volume pricing for that pattern is on the pricing page.
Field-tested guidance from helpdesk redaction rollouts.
The window that matters is between ticket creation and the moment agents, integrations, and exports copy the content onward. Scan on the creation webhook, not on a nightly batch — a card number that sat visible for eighteen hours has already been synced to the warehouse and read by a dozen people. Keep the nightly job too, but as a verification sweep that should find nothing.
Redaction that surprises agents gets worked around — screenshots, side channels, "please resend your card number". Involve support leads in choosing what stays visible, use typed placeholders so context survives, provide unmask-on-demand for the rare legitimate need, and give agents a macro that tells customers their data was removed for their protection. Redaction then becomes a service agents sell, not a control they fight.
Detection metrics tell you where over-sharing originates. If refund forms drive card-number pastes, add a "never send full card numbers" hint and a structured last-4 field. If a chatbot hand-off causes users to repeat their details, pass context through instead. Every inflow you fix reduces scanning volume and risk permanently — the detector's per-channel entity counts are your prioritized to-do list.
custom_instruction to tell the detector to ignore the requester's own name and your support addresses during ingest scans so they do not generate noise, while still catching third-party names in the body.PASSWORD, API_KEY, AUTH_TOKEN) in the agent-side scan scope.Paste a real (anonymized) ticket into the live demo to see exactly what the API would catch, then wire the webhook in an afternoon. Plans scale from a single desk to millions of tickets.
Try the Live Demo View Pricing