From zero to detecting PII in production: quickstart, drop-in Python and Node.js wrappers, pipeline and webhook patterns, and how to test everything against the live demo.
The PII Detection API finds, classifies, and locates sensitive data in text: 150+ entity types across personal, financial, medical/PHI, credential, location, device/network, and demographic categories, in 60+ languages. Each detected entity comes back with its type, the matched text, exact character offsets, and a confidence score.
https://piidetectionapi.com/api/moderate.php — all requests are POST with a JSON body.Authentication is done by including your api_key in the JSON request body. Every request must also include "api_type": "pii_detection". Get your key on the get started page.
YOUR_API_KEY in all examples below with your own API key. Never expose your API key in client-side code or public repositories — call the API from your backend.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": "Contact John Doe at [email protected] or 555-123-4567." }'
Response:
{
"detected_entities": [
{ "type": "PERSON_NAME", "text": "John Doe", "start": 8, "end": 16, "confidence": 0.95 },
{ "type": "EMAIL_ADDRESS", "text": "[email protected]", "start": 20, "end": 36, "confidence": 0.98 },
{ "type": "PHONE_NUMBER", "text": "555-123-4567", "start": 40, "end": 52, "confidence": 0.97 }
],
"anonymized_text": "Contact [NAME] at [EMAIL] or [PHONE].",
"entities_detected": 3,
"processing_time_ms": 187,
"mask_mode_used": "replace",
"status": 200
}All request fields:
| Parameter | Type | Description | |
|---|---|---|---|
api_key | string | Required | Your API key for authentication. |
api_type | string | Required | Always "pii_detection". |
text | string | Required | The content to scan. Up to 50,000 characters per request. |
entities | array | Optional | Entity types to detect, e.g. ["PERSON_NAME","EMAIL_ADDRESS","SSN"]. If omitted, all 150+ types are detected. Full catalog: entities.php. |
exclude_entities | array | Optional | Entity types to skip — neither reported nor masked. Example: ["MEDICAL_TERM", "TREATMENT"]. |
mask_mode | string | Optional | "replace" (default) — [TYPE] placeholders."redact" — removes the match."hash" — consistent hashes (same value, same token). |
threshold | number | Optional | Minimum confidence 0–1 for an entity to be returned. Default 0.5. |
custom_instruction | string | Optional | Natural-language exclusions to preserve specific terms. Max 500 characters. Paid plans only. |
Every successful response (status 200) includes:
| Field | Type | Description |
|---|---|---|
detected_entities | array | The detection results — each entity with type, text, start, end, confidence. Offsets are zero-based and end-exclusive: text == input[start:end]. |
anonymized_text | string | The input with detected entities masked according to mask_mode. |
entities_detected | int | Number of entities found. |
processing_time_ms | int | Server-side processing time in milliseconds. |
mask_mode_used | string | The masking strategy that was applied. |
status | int | HTTP-style status code (200 for success). |
A small client class you can drop into any project: typed helpers for detection, redaction, and a boolean "contains PII?" check, with retries built in.
# pip install requests import os, time import requests class PIIDetectionClient: """Minimal client for the PII Detection API.""" API_URL = "https://piidetectionapi.com/api/moderate.php" def __init__(self, api_key=None, timeout=60, retries=3): self.api_key = api_key or os.environ["PII_API_KEY"] self.timeout = timeout self.retries = retries def detect(self, text, entities=None, exclude_entities=None, mask_mode="replace", threshold=0.5, custom_instruction=None): payload = { "api_key": self.api_key, "api_type": "pii_detection", "text": text, "mask_mode": mask_mode, "threshold": threshold, } if entities: payload["entities"] = entities if exclude_entities: payload["exclude_entities"] = exclude_entities if custom_instruction: payload["custom_instruction"] = custom_instruction for attempt in range(self.retries + 1): resp = requests.post(self.API_URL, json=payload, timeout=self.timeout) data = resp.json() if data["status"] == 200: return data if data["status"] == 429 or data["status"] >= 500: time.sleep(2 ** attempt) # retryable: backoff continue raise RuntimeError(f"PII API error {data['status']}: {data.get('error')}") raise RuntimeError("PII API: max retries exceeded") def contains_pii(self, text, threshold=0.5): """True if any entity is detected at or above the threshold.""" return self.detect(text, threshold=threshold)["entities_detected"] > 0 def redact(self, text, mask_mode="replace"): """Return only the masked text.""" return self.detect(text, mask_mode=mask_mode)["anonymized_text"] # Usage client = PIIDetectionClient() result = client.detect("Contact John Doe at [email protected] or 555-123-4567.") for e in result["detected_entities"]: print(e["type"], e["text"], e["start"], e["end"], e["confidence"]) if client.contains_pii(user_message): safe_text = client.redact(user_message)
The same wrapper as an ES module for Node 18+ (built-in fetch). Export one client instance and reuse it across your app.
const API_URL = 'https://piidetectionapi.com/api/moderate.php'; export class PIIDetectionClient { constructor({ apiKey = process.env.PII_API_KEY, timeoutMs = 60000, retries = 3 } = {}) { this.apiKey = apiKey; this.timeoutMs = timeoutMs; this.retries = retries; } async detect(text, options = {}) { const payload = { api_key: this.apiKey, api_type: 'pii_detection', text, mask_mode: options.maskMode ?? 'replace', threshold: options.threshold ?? 0.5, }; if (options.entities) payload.entities = options.entities; if (options.excludeEntities) payload.exclude_entities = options.excludeEntities; if (options.customInstruction) payload.custom_instruction = options.customInstruction; for (let attempt = 0; attempt <= this.retries; attempt++) { const res = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: AbortSignal.timeout(this.timeoutMs) }); const data = await res.json(); if (data.status === 200) return data; if (data.status === 429 || data.status >= 500) { await new Promise(r => setTimeout(r, 2 ** attempt * 1000)); continue; } throw new Error(`PII API error ${data.status}: ${data.error}`); } throw new Error('PII API: max retries exceeded'); } async containsPII(text, threshold = 0.5) { const { entities_detected } = await this.detect(text, { threshold }); return entities_detected > 0; } async redact(text, maskMode = 'replace') { const { anonymized_text } = await this.detect(text, { maskMode }); return anonymized_text; } } // Usage import { PIIDetectionClient } from './piiClient.mjs'; const pii = new PIIDetectionClient(); const result = await pii.detect('Contact John Doe at [email protected]', { entities: ['PERSON_NAME', 'EMAIL_ADDRESS'] }); console.log(result.detected_entities);
For PHP backends, a single function wraps the whole API. Store the key in an environment variable and call it wherever user content enters your system.
<?php function detect_pii(string $text, array $options = []): array { $payload = array_merge([ 'api_key' => getenv('PII_API_KEY'), 'api_type' => 'pii_detection', 'text' => $text, ], $options); $ch = curl_init('https://piidetectionapi.com/api/moderate.php'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_TIMEOUT => 60, ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); return $data ?? ['status' => 500, 'error' => 'no response']; } // Usage: scan a support ticket before saving it $result = detect_pii($ticketBody, [ 'entities' => ['PERSON_NAME', 'EMAIL_ADDRESS', 'PHONE_NUMBER', 'CREDIT_CARD_NUMBER'], 'mask_mode' => 'replace', ]); if ($result['status'] === 200 && $result['entities_detected'] > 0) { $safeBody = $result['anonymized_text']; }
By default the API scans for everything. Two knobs narrow the results to what your workflow acts on:
entities restricts detection to the listed types — smaller responses, explicit policy:
result = client.detect(
"Card 4111-1111-1111-1111, IBAN GB29 NWBK 6016 1331 9268 19, routing 021000021.",
entities=["CREDIT_CARD_NUMBER", "IBAN_CODE", "ROUTING_NUMBER", "CVV_NUMBER"],
)threshold trades recall for precision. Discovery scans (DLP, compliance audits) run low; automated redaction runs high:
result = client.detect(document, threshold=0.3) # catch everything plausible auto_redact = [e for e in result["detected_entities"] if e["confidence"] >= 0.8] human_review = [e for e in result["detected_entities"] if e["confidence"] < 0.8]
Detection results are always the same; mask_mode only changes how anonymized_text is rendered:
| mask_mode | Behavior | Example output |
|---|---|---|
"replace" | Typed placeholders — readable and auditable | Contact [NAME] at [EMAIL] or [PHONE]. |
"redact" | Removes the matched value entirely | Contact at or . |
"hash" | Consistent hashes — the same value always yields the same token, so counts and joins survive masking | Contact [HASH_NAME_A1B2C3] at [HASH_EMAIL_F4E5D6] or [HASH_PHONE_K9L8M7]. |
text = "Contact John Doe at [email protected] or 555-123-4567." for mode in ["replace", "redact", "hash"]: result = client.detect(text, mask_mode=mode) print(f"{mode:8s} -> {result['anonymized_text']}")
Need a custom format — partial masking, your own placeholder scheme, HTML highlighting? Build it from the offsets in detected_entities; the extended documentation has a complete redaction-pipeline tutorial.
Two complementary tools keep non-sensitive content out of your results:
exclude_entities skips whole categories. Anything matching those types is neither reported nor masked:
result = client.detect(
"The patient saw the doctor at the hospital. Dr. Smith ordered tests for John Doe.",
exclude_entities=["MEDICAL_TERM", "TREATMENT", "DIAGNOSIS"],
)
print(result["anonymized_text"])
# The patient saw the doctor at the hospital. Dr. [NAME] ordered tests for [NAME].custom_instruction preserves specific terms in plain English (max 500 characters, paid plans):
result = client.detect(
"Contact John Doe at Acme Corporation, 123 Business Ave. Email: [email protected]",
custom_instruction="Do not flag 'Acme Corporation' or '123 Business Ave' as these are our company details.",
)
print(result["anonymized_text"])
# Contact [NAME] at Acme Corporation, 123 Business Ave. Email: [EMAIL]exclude_entities. Specific words or phrases → custom_instruction. They can be combined in one request.The API is stateless and fast, so it slots into event-driven systems as a filter step. Three patterns cover most deployments:
Pattern A — inline gate (webhooks, chat, form submissions). Scan the payload the moment it arrives; block, mask, or route based on what is found:
app.post('/webhook/ticket-created', async (req, res) => { const result = await pii.detect(req.body.ticket.description, { entities: ['PERSON_NAME', 'EMAIL_ADDRESS', 'PHONE_NUMBER', 'CREDIT_CARD_NUMBER', 'SSN'], maskMode: 'replace' }); if (result.entities_detected > 0) { // store the masked copy; log types + offsets only, never raw values await saveTicket({ ...req.body.ticket, description: result.anonymized_text }); await auditLog(req.body.ticket.id, result.detected_entities.map(({ type, start, end }) => ({ type, start, end }))); } else { await saveTicket(req.body.ticket); } res.sendStatus(200); // respond quickly; keep heavy work async });
Pattern B — queue consumer (Kafka, SQS, ETL). Put the detection call inside your consumer so PII is found before records land in the warehouse. Batch with bounded concurrency and requeue on 429/5xx. Full architectures: PII detection in ETL & streaming pipelines.
Pattern C — LLM guardrail. Scan prompts before they reach a model and completions before they reach users; block or mask based on entities_detected. Walkthrough: PII detection for LLM guardrails.
Before writing integration code, use the live demo to explore how detection behaves on your real data shapes. Paste representative samples — a support ticket, a log line, a clinical note — and check:
entities list matches reality;threshold;exclude_entities or custom_instruction.Then reproduce the same configuration in code and add a smoke test to CI: a fixture string with known PII, asserting that the expected types are detected and that offsets slice back to the matched text. Sample texts in other languages are worth testing too — see supported languages.
def test_detects_known_pii(): text = "Contact John Doe at [email protected] or 555-123-4567." result = client.detect(text) types = {e["type"] for e in result["detected_entities"]} assert {"PERSON_NAME", "EMAIL_ADDRESS", "PHONE_NUMBER"} <= types for e in result["detected_entities"]: assert text[e["start"]:e["end"]] == e["text"]
The API returns descriptive error messages. Always check the status field in the response:
| Status | Meaning | What to do |
|---|---|---|
200 | Success | Process detected_entities. |
400 | Bad Request | Fix the request: missing api_type or text, invalid mask_mode, or unknown entity type. Do not retry unchanged. |
401 | Unauthorized | Check the API key. Do not retry unchanged. |
402 | Insufficient Credits | Top up credits or wait for the quota reset. |
413 | Payload Too Large | Text exceeds 50,000 characters — chunk the document. |
429 | Rate Limited | Retry with exponential backoff; honor Retry-After. |
500 | Server Error | Retry with exponential backoff. |
The wrappers in sections 3 and 4 already implement the retry policy: backoff on 429/5xx, immediate failure on 4xx. Two additional practices for production:
Everything referenced in this guide, in one place:
Try the live demo on your own text, then grab an API key and ship your first detection call in minutes.
Try the Demo View Pricing