PII Redaction API — developer reference
Base URL: https://privacyfilter.run/api. Authenticate with a license_key UUID in the JSON body. One endpoint for single texts (POST /redact), one for batch (POST /redact/batch). Detect and remove PII in under 1 second per document. Available on any paid plan ($9 one-time or $19/month).
The PrivacyFilter API is a hosted REST service for detecting and removing Personally Identifiable Information (PII) from text. It is the same engine that powers the web tool — OpenAI Privacy Filter under the hood — exposed as a simple JSON API your application can call from any language.
This page is the complete reference for all endpoints, request/response schemas, error codes, and rate limits.
Authentication
All API requests use a license key — a UUID generated when you purchase a plan. Include it as "license_key" in the JSON body of every request. There is no Bearer token header or OAuth flow.
Get your license key at privacyfilter.run after purchasing. Keys are delivered by email and are active immediately.
Endpoints
POST /api/redact — single document
Detects and redacts PII in a single text string. Returns the redacted text plus a list of entities with their original values, types, and character offsets.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | The text to redact. Max 10,000 chars (paid), 2,000 chars (free, no key). |
license_key | UUID string | Paid features | Your plan license key. Required for >2,000 chars, batch, or JSON export. |
mode | string | No | "replace" (default), "mask", or "tag". |
Redaction modes
replace— replaces each PII entity with a labeled placeholder like[PERSON_1],[EMAIL_2],[PHONE_3]. The number is a consistent index within the document, so the same person always gets the same label.mask— replaces each entity with a block of █ characters matching the original length. Irreversible.tag— wraps entities with<PII type="PERSON">John Smith</PII>. Useful for downstream parsing.
Request example
curl -X POST https://privacyfilter.run/api/redact \
-H "Content-Type: application/json" \
-d '{
"text": "Hi, I am John Smith (john@example.com, +1 555-0192). SSN: 123-45-6789.",
"license_key": "YOUR-UUID-HERE",
"mode": "replace"
}'
Response schema
{
"redacted_text": "Hi, I am [PERSON_1] ([EMAIL_2], [PHONE_3]). SSN: [SSN_4].",
"entity_count": 4,
"char_count": 71,
"entities": [
{ "index": 1, "type": "PERSON", "original": "John Smith", "replacement": "[PERSON_1]", "start": 9, "end": 19 },
{ "index": 2, "type": "EMAIL", "original": "john@example.com","replacement": "[EMAIL_2]", "start": 21, "end": 37 },
{ "index": 3, "type": "PHONE", "original": "+1 555-0192", "replacement": "[PHONE_3]", "start": 39, "end": 50 },
{ "index": 4, "type": "SSN", "original": "123-45-6789", "replacement": "[SSN_4]", "start": 58, "end": 69 }
],
"processing_ms": 420
}
POST /api/redact/batch — multiple documents
Processes up to 20 documents in a single request. Each document is processed independently. Requires a paid license key.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
documents | array | Yes | Array of objects: {"id": string, "text": string}. Max 20 items. |
license_key | UUID string | Yes | Your plan license key. |
mode | string | No | "replace" (default), "mask", or "tag". Applied to all documents. |
Python example
import httpx
LICENSE_KEY = "your-uuid-here"
documents = [
{"id": "ticket_001", "text": "Customer Alice Brown (alice@shop.io) reported issue #1042"},
{"id": "ticket_002", "text": "Follow-up from Bob Lee, +44 7911 123456, RE: order 8821"},
]
resp = httpx.post(
"https://privacyfilter.run/api/redact/batch",
json={"documents": documents, "license_key": LICENSE_KEY, "mode": "replace"},
timeout=60,
)
resp.raise_for_status()
for result in resp.json()["results"]:
print(f"{result['id']}: {result['redacted_text']}")
Batch response schema
{
"results": [
{
"id": "ticket_001",
"redacted_text": "Customer [PERSON_1] ([EMAIL_2]) reported issue #1042",
"entity_count": 2,
"entities": [ ... ]
},
{
"id": "ticket_002",
"redacted_text": "Follow-up from [PERSON_1], [PHONE_2], RE: order 8821",
"entity_count": 2,
"entities": [ ... ]
}
],
"total_processed": 2,
"total_entities": 4
}
Entity types
| Type string | What it detects | Example |
|---|---|---|
PERSON | Full names, first names in context | John Smith, Maria |
EMAIL | Email addresses | user@company.com |
PHONE | Phone numbers (international formats) | +1 555-0192, 07911 123456 |
ADDRESS | Postal addresses | 14 Elm St, Boston MA 02118 |
SSN | Social Security Numbers and national IDs | 123-45-6789 |
DATE_OF_BIRTH | Dates of birth (in context) | born January 14, 1985 |
CREDIT_CARD | Credit/debit card numbers | 4111 1111 1111 1111 |
IP_ADDRESS | IPv4 and IPv6 addresses | 192.168.1.1 |
URL | Personal URLs, profile links | linkedin.com/in/johndoe |
Rate limits
| Plan | Single redact | Batch redact | Max chars |
|---|---|---|---|
| Free (no key) | 3/day | Not available | 2,000 |
| Redact Pack ($9) | 50 total | Up to 5 docs | 10,000 |
| Unlimited ($19/mo) | Unlimited | Up to 20 docs | 10,000 |
Error codes
| HTTP status | Code | Meaning |
|---|---|---|
| 400 | TEXT_TOO_LONG | Text exceeds the character limit for your plan. |
| 401 | INVALID_LICENSE | License key not found or expired. |
| 402 | QUOTA_EXHAUSTED | All credits in a Redact Pack have been used. |
| 429 | RATE_LIMITED | Too many requests. Free tier: 3/day. Back off and retry after the Retry-After header value. |
| 500 | UPSTREAM_ERROR | Upstream model error. Retry with exponential back-off. |
Node.js example
const response = await fetch('https://privacyfilter.run/api/redact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: 'Contact Sarah Jones at sarah@startup.io or 0208 555 1234.',
license_key: process.env.PRIVACY_FILTER_KEY,
mode: 'replace',
}),
});
const { redacted_text, entities } = await response.json();
console.log(redacted_text);
// → 'Contact [PERSON_1] at [EMAIL_2] or [PHONE_3].'
Re-inserting original values (pseudonymization pattern)
When you need personalized LLM output (e.g., draft a reply that addresses the customer by name), use mode=replace and then re-substitute placeholders back into the model's output:
import httpx, openai
def scrub_then_complete(raw_text, system_prompt):
r = httpx.post("https://privacyfilter.run/api/redact",
json={"text": raw_text, "license_key": LICENSE_KEY, "mode": "replace"},
timeout=15).raise_for_status().json()
entity_map = {e["replacement"]: e["original"] for e in r["entities"]}
clean = r["redacted_text"]
reply = openai.OpenAI().chat.completions.create(
model="gpt-4o",
messages=[{"role":"system","content":system_prompt},
{"role":"user","content":clean}]
).choices[0].message.content
for placeholder, original in entity_map.items():
reply = reply.replace(placeholder, original)
return reply
The entity map — {"[PERSON_1]": "Alice Brown", "[EMAIL_2]": "alice@shop.io"} — is returned in every replace-mode response. Keep it to re-personalize LLM outputs after the model has processed clean text.
Get your API key — Redact Pack ($9, 50 calls) or Unlimited ($19/mo). Instant activation.