Learn how to automatically find, classify, and locate email addresses in text, logs, documents, and chat transcripts using PII Detection API. Catch standard formats, plus-addressing, and obfuscated variants like "john at example dot com" that regex-based scanners miss.
Email addresses are among the most widespread — and most underestimated — categories of personally identifiable information. They appear in nearly every dataset an organization touches: CRM exports, support tickets, application logs, chat transcripts, sign-up forms, survey free-text fields, e-discovery corpora, and increasingly in the prompts users type into LLM-powered assistants. Unlike a name, which may be ambiguous, an email address is a globally unique identifier that resolves to exactly one mailbox and, in most cases, exactly one person.
PII Detection API detects email addresses with the EMAIL_ADDRESS entity type. For every match it returns the entity type, the exact matched text, the character offsets where it starts and ends, and a confidence score — so you can locate the address precisely in the original string, not just know that one exists somewhere. Optionally, the API also returns a masked version of the input in the same response, so detection and redaction happen in a single call.
Because the detection engine is a context-aware AI model rather than a single regular expression, it recognizes far more than the textbook [email protected] pattern. It catches deliberately obfuscated addresses ("sarah dot lee at gmail dot com"), plus-addressed variants ([email protected]), addresses buried inside URLs and log lines, and internationalized addresses with non-Latin characters — across 60+ languages and alongside 150+ other entity types listed on our entities page.
An email address is a direct identifier. Under GDPR it is unambiguously personal data; under CCPA/CPRA it is personal information; under HIPAA it is one of the 18 identifiers that must be removed for Safe Harbor de-identification. Regulators treat email addresses seriously precisely because they are so linkable: a single address can join together a person's activity across dozens of otherwise separate systems and datasets.
Beyond regulation, email addresses leak into places they should never be. Developers log full request payloads and end up with customer addresses in plaintext log aggregators. Support agents paste customer correspondence into internal wikis. Analytics pipelines forward sign-up events — email included — to third-party tools. Each of these is a quiet breach waiting to be discovered. In credential-stuffing attacks, a leaked email address is half of a login pair; combined with a reused password from another breach, it becomes an account takeover.
Tip: Email addresses often carry a person's full name in the local part (jane.doe@…). Redacting the name but leaving the address defeats the purpose — always scan for both PERSON_NAME and EMAIL_ADDRESS together.
Nearly every engineer has written an email regex, and nearly every email regex is wrong. The formal grammar in RFC 5322 permits addresses that are dramatically more varied than [email protected], and the modern internet has stretched the format further with hundreds of new top-level domains and internationalized domain names. A pattern strict enough to avoid false positives will miss real addresses; a pattern loose enough to catch everything will flag file paths, Twitter handles, and code snippets.
"john..doe"@example.com and even "john smith"@example.com are valid per RFC 5322.[email protected], [email protected], [email protected], o'[email protected] — apostrophes, hyphens, and underscores are all legal.[email protected] — multiple labels on the domain side are common in corporate mail.[email protected] or [email protected]. There are over 1,500 TLDs today.用户@例え.jp or müller@bücher.de — IDN domains and UTF-8 local parts (RFC 6531) are fully valid and increasingly common outside the English-speaking world.PII Detection API resolves these cases with a transformer-based model that reads the surrounding context, not just the character pattern. It understands that [email protected]. at the end of a sentence excludes the period, that [email protected]:user/repo.git in a clone command is an SSH remote rather than a personal mailbox, and that @support in a chat message is a mention, not an address. You can read more about the pattern-versus-AI tradeoff in our NER vs regex comparison guide.
Warning: "Just use a stricter regex" is a trap. Every tightening that removes a false positive class introduces a false negative class. For compliance scanning, a missed address is usually the more expensive error — a leaked identifier — while a false positive merely over-redacts one token.
Detecting email addresses takes a single POST request. Send your text with the entities list restricted to EMAIL_ADDRESS, and the API returns every detected address with offsets and confidence scores, plus a masked copy of the text. Try it interactively in the live demo, or grab a key from the pricing page and run the snippets below.
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": "Send the contract to [email protected] and cc [email protected].", "entities": ["EMAIL_ADDRESS"], "mask_mode": "replace" }'
import requests resp = requests.post( "https://piidetectionapi.com/api/moderate.php", json={ "api_key": "YOUR_API_KEY", "api_type": "pii_detection", "text": "Send the contract to [email protected] and cc [email protected].", "entities": ["EMAIL_ADDRESS"], "mask_mode": "replace", }, timeout=30, ) data = resp.json() for e in data["detected_entities"]: print(e["type"], e["text"], e["start"], e["end"], e["confidence"])
const resp = await fetch("https://piidetectionapi.com/api/moderate.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: "YOUR_API_KEY", api_type: "pii_detection", text: "Send the contract to [email protected] and cc [email protected].", entities: ["EMAIL_ADDRESS"], mask_mode: "replace", }), }); const data = await resp.json(); data.detected_entities.forEach(e => console.log(e.type, e.text, e.start, e.end, e.confidence) );
The response contains the structured entity list and the masked text in one payload:
{
"detected_entities": [
{"type": "EMAIL_ADDRESS", "text": "[email protected]", "start": 21, "end": 51, "confidence": 0.99},
{"type": "EMAIL_ADDRESS", "text": "[email protected]", "start": 59, "end": 85, "confidence": 0.98}
],
"anonymized_text": "Send the contract to [EMAIL_ADDRESS] and cc [EMAIL_ADDRESS].",
"entities_detected": 2,
"processing_time_ms": 142,
"mask_mode_used": "replace",
"status": 200
}
A few notes on the request fields: entities is optional — omit it to scan for all 150+ entity types at once; here we restrict it to EMAIL_ADDRESS for a focused scan. Requests accept up to 50,000 characters of text. Full parameter documentation lives in the API reference.
People deliberately mangle email addresses to evade scrapers — and, inadvertently, to evade your compliance scanner. Forum users write "contact me: john at example dot com". Sellers on marketplaces write "john[at]example[dot]com" to dodge platform rules against sharing contact details. Users of moderated chat platforms space out characters ("j o h n @ e x a m p l e . c o m") to slip addresses past keyword filters. From a privacy standpoint these are all fully functional identifiers: any human reader can reconstruct the mailbox instantly.
This is where pattern matching fails hardest. A regex tuned for [email protected] sees no @ sign in "john at example dot com" and moves on. The context-aware model behind PII Detection API instead recognizes the communicative intent: a token sequence that a person would read as an email address is flagged as one, whatever character substitutions were used.
| Written Form | Example | Naive Regex | PII Detection API |
|---|---|---|---|
| Standard | [email protected] | Detected | Detected |
| Spelled-out separators | john at example dot com | Missed | Detected |
| Bracketed separators | john[at]example[dot]com | Missed | Detected |
| Uppercase separators | john AT example DOT com | Missed | Detected |
| Parenthesized | john(at)example(dot)com | Missed | Detected |
| Spaced characters | j o h n @ example . com | Missed | Detected |
| Inside URL / mailto | mailto:[email protected]?subject=Hi | Partial | Detected |
| IDN / unicode domain | müller@bücher.de | Missed | Detected |
The following example runs an obfuscated-heavy sample through the API. Note that nothing changes in the request — obfuscation handling is built into the EMAIL_ADDRESS detector, so trust-and-safety teams can use the same call for chat moderation as compliance teams use for log scanning:
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": "DM me: sarah [at] fastmail [dot] com. Backup is s a r a h 9 9 @ g m a i l . c o m", "entities": ["EMAIL_ADDRESS"], "threshold": 0.4 }'
import requests resp = requests.post( "https://piidetectionapi.com/api/moderate.php", json={ "api_key": "YOUR_API_KEY", "api_type": "pii_detection", "text": "DM me: sarah [at] fastmail [dot] com. Backup is s a r a h 9 9 @ g m a i l . c o m", "entities": ["EMAIL_ADDRESS"], "threshold": 0.4, # lower threshold widens the net for evasive formats }, timeout=30, ) for e in resp.json()["detected_entities"]: print(f"{e['text']!r} -> confidence {e['confidence']}")
const resp = await fetch("https://piidetectionapi.com/api/moderate.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: "YOUR_API_KEY", api_type: "pii_detection", text: "DM me: sarah [at] fastmail [dot] com. Backup is s a r a h 9 9 @ g m a i l . c o m", entities: ["EMAIL_ADDRESS"], threshold: 0.4, // lower threshold widens the net for evasive formats }), }); const { detected_entities } = await resp.json(); detected_entities.forEach(e => console.log(e.text, e.confidence));
Tip: Obfuscated addresses naturally score lower confidence than clean ones. If your use case is adversarial (marketplace moderation, chat filtering), lower threshold to 0.3–0.4 to prioritize recall; for low-stakes analytics scrubbing, the default 0.5 is a good balance.
Not every string with an @ sign carries the same privacy weight, and mature detection pipelines treat the categories differently. Understanding the distinctions helps you decide what to redact, what to keep, and what to route through a custom_instruction exclusion.
Addresses like [email protected] or [email protected] identify a specific human being and are personal data everywhere personal data is regulated. Corporate addresses are not exempt: EU regulators have been explicit that [email protected] is personal data of the employee, because it identifies a natural person even though the domain belongs to a business. When in doubt, treat any address whose local part encodes a human name as PII.
Addresses such as info@, support@, sales@, or noreply@ point to a function rather than a person. In many pipelines these are noise you do not want redacted — masking your own support address in every ticket destroys analytical value while protecting no one. Rather than post-processing the entity list, you can tell the API directly with a natural-language exclusion:
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": "Ticket #4821: customer [email protected] emailed [email protected] about billing.", "entities": ["EMAIL_ADDRESS"], "custom_instruction": "Do not flag role-based addresses at ourcompany.com such as support@, info@, or billing@." }'
import requests resp = requests.post( "https://piidetectionapi.com/api/moderate.php", json={ "api_key": "YOUR_API_KEY", "api_type": "pii_detection", "text": "Ticket #4821: customer [email protected] emailed [email protected] about billing.", "entities": ["EMAIL_ADDRESS"], "custom_instruction": "Do not flag role-based addresses at ourcompany.com such as support@, info@, or billing@.", }, timeout=30, ) print(resp.json()["anonymized_text"]) # Ticket #4821: customer [EMAIL_ADDRESS] emailed [email protected] about billing.
const resp = await fetch("https://piidetectionapi.com/api/moderate.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: "YOUR_API_KEY", api_type: "pii_detection", text: "Ticket #4821: customer [email protected] emailed [email protected] about billing.", entities: ["EMAIL_ADDRESS"], custom_instruction: "Do not flag role-based addresses at ourcompany.com such as support@, info@, or billing@.", }), }); const data = await resp.json(); console.log(data.anonymized_text); // Ticket #4821: customer [EMAIL_ADDRESS] emailed [email protected] about billing.
Gmail, Fastmail, Outlook, and most modern providers support subaddressing: [email protected], [email protected], and [email protected] all deliver to the same inbox. For privacy purposes the plus tag makes the address more identifying, not less — the tag often reveals which service the address was given to, which is exactly the kind of behavioral linkage data brokers exploit. The API detects the full plus-addressed form as a single EMAIL_ADDRESS entity, tag included. If you deduplicate users by email, normalize plus-tags after detection using the returned offsets — never before scanning, or the offsets will no longer align with your original text.
Addresses at burner services (mailinator.com, guerrillamail.com, temp-mail.org and thousands of rotating clones) are structurally normal emails and are detected as such. Whether they are meaningfully "personal" is a policy question for your team — many pipelines still redact them because the local part frequently reuses a handle the person uses elsewhere.
Detection tells you where the addresses are; mask_mode decides what happens to them in the returned anonymized_text. Three modes cover the common workflows:
[EMAIL_ADDRESS]. Best when downstream readers or models should know an email was there without seeing it.Hashing deserves special attention for email addresses, because emails are the join key of most customer datasets. With mask_mode: "hash" you can count distinct users, trace one anonymous user across sessions, and correlate tickets — all without a single readable address in the dataset:
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": "[email protected] opened ticket 4821. Follow-up from [email protected] on Tuesday.", "entities": ["EMAIL_ADDRESS"], "mask_mode": "hash" }'
import requests resp = requests.post( "https://piidetectionapi.com/api/moderate.php", json={ "api_key": "YOUR_API_KEY", "api_type": "pii_detection", "text": "[email protected] opened ticket 4821. Follow-up from [email protected] on Tuesday.", "entities": ["EMAIL_ADDRESS"], "mask_mode": "hash", }, timeout=30, ) print(resp.json()["anonymized_text"]) # Both mentions map to the SAME hash token, so per-user analytics still work.
const resp = await fetch("https://piidetectionapi.com/api/moderate.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: "YOUR_API_KEY", api_type: "pii_detection", text: "[email protected] opened ticket 4821. Follow-up from [email protected] on Tuesday.", entities: ["EMAIL_ADDRESS"], mask_mode: "hash", }), }); const data = await resp.json(); console.log(data.anonymized_text); // Both mentions map to the SAME hash token, so per-user analytics still work.
Note: Hashed email addresses are pseudonymized, not anonymized. Because the input space of real addresses is enumerable, a determined attacker with a candidate list can test guesses against hashes. Under GDPR, hashed emails generally remain personal data — treat the hashed dataset accordingly.
Email addresses rarely travel alone. The same sentence that contains an address usually contains a name, a phone number, or both. Restricting your scan to a single entity type creates the illusion of clean data while other identifiers pass through. A pragmatic baseline set for communications data is:
json={
"api_key": "YOUR_API_KEY",
"api_type": "pii_detection",
"text": text,
"entities": ["EMAIL_ADDRESS", "PERSON_NAME", "PHONE_NUMBER", "URL"],
}
The cheapest place to detect an email address is before it lands anywhere permanent: in the logging middleware, the event-pipeline transform, or the pre-prompt hook of your LLM integration. Retrofitting detection over a data lake that has been accumulating raw addresses for years is possible — but it is a discovery project, not a filter. If you are building the pipeline case, our log scanning guide walks through middleware placement in detail.
When you post-process results yourself, always splice by the returned start/end offsets rather than doing find-and-replace on the matched text. String replacement corrupts data when the same address appears with different casing, or when the matched substring happens to occur inside a longer token.
Before production rollout, run a few hundred representative records and review the matches. Check both directions: addresses missed (false negatives — often obfuscated or truncated forms) and non-addresses flagged (false positives — occasionally SSH remotes or decorated code identifiers). Adjust threshold or add a custom_instruction based on what you see. Our guide on measuring detection accuracy covers how to build this evaluation loop properly.
A surprisingly common failure: the scrubbing service itself logs its raw input for debugging. Make sure request bodies to your detection layer are exempt from payload logging, or you have simply moved the leak one service downstream.
Addresses frequently appear as query parameters ([email protected]), in mailto: links, or embedded in structured log fields. The detector flags the address portion with exact offsets, so masking surgically removes the address while leaving the surrounding URL or log structure parseable. If you want whole URLs flagged as well, add URL to your entity list.
Technical text is full of email-shaped strings that are not personal mailboxes: [email protected]:org/repo.git, package maintainer fields, example addresses in documentation ([email protected]). Context awareness suppresses most of these, and RFC 2606 reserved domains like example.com can be excluded explicitly with a custom_instruction such as "ignore addresses at example.com, example.org and test domains".
UI truncation and copy-paste accidents produce fragments like jane.doe@gm… or @example.com. A fragment that no longer identifies a mailbox is usually not flagged; a fragment that plainly does (a full local part plus a recognizable truncated domain) may be, at reduced confidence. Decide by policy whether your threshold should include these.
Detection is per-mention: five addresses yield five entities, even if they belong to one person. If your workflow needs person-level grouping (say, for a data subject request), group by normalized address after detection — and remember plus-tag and dot-variant normalization for Gmail-style providers.
Tip: For a quick sanity check on any tricky sample, paste it into the interactive demo — it shows every detected entity with its confidence score, which is the fastest way to see how the model treats your edge cases.
Yes. Obfuscated forms — spelled-out separators, bracketed variants like [at]/[dot], parenthesized forms, uppercase AT/DOT, and character-spaced addresses — are detected by the same EMAIL_ADDRESS entity. Because the model reads context rather than matching characters, it flags anything a human reader would reconstruct as an address. Heavily creative obfuscation scores lower confidence, so lower the threshold for adversarial content.
No — it is detected as one EMAIL_ADDRESS entity including the plus tag. Note that the tag can itself be revealing (it often names the service the address was registered with), which is a reason to redact rather than preserve it. If you deduplicate users, normalize plus-tags in your own post-processing using the returned offsets.
Usually yes. An address like [email protected] identifies a specific employee and is personal data under GDPR and most privacy laws, even though the domain belongs to a company. Purely role-based addresses (info@, support@) identify a function rather than a person and are commonly excluded — you can do that in one line with custom_instruction.
The detector is calibrated for identifiability, not deliverability. It will flag a plausible address even if the mailbox happens not to exist, because the string still reveals a person's naming pattern and provider. Conversely, email-shaped technical strings such as git SSH remotes are generally suppressed by context. The API does not perform SMTP validation.
Yes — use mask_mode: "hash". The same input address always produces the same hash token, so distinct counts, joins, and per-user analytics survive redaction. Keep in mind that hashed addresses are pseudonymous rather than anonymous, and should still be handled as regulated data in most jurisdictions.
Yes. The model operates across 60+ languages, and internationalized addresses — IDN domains like bücher.de or fully non-Latin addresses — are supported. Surrounding-language context ("écrivez-moi à …", "メールは…まで") actually helps the model catch obfuscated forms in that language.
Try the live demo with your own text, or get an API key and integrate email detection in minutes.
Try the Live Demo View Pricing