← Blog  ·  April 30, 2026  ·  7 min read

PII Redaction API — developer reference

Quick start

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

FieldTypeRequiredDescription
textstringYesThe text to redact. Max 10,000 chars (paid), 2,000 chars (free, no key).
license_keyUUID stringPaid featuresYour plan license key. Required for >2,000 chars, batch, or JSON export.
modestringNo"replace" (default), "mask", or "tag".

Redaction modes

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

FieldTypeRequiredDescription
documentsarrayYesArray of objects: {"id": string, "text": string}. Max 20 items.
license_keyUUID stringYesYour plan license key.
modestringNo"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 stringWhat it detectsExample
PERSONFull names, first names in contextJohn Smith, Maria
EMAILEmail addressesuser@company.com
PHONEPhone numbers (international formats)+1 555-0192, 07911 123456
ADDRESSPostal addresses14 Elm St, Boston MA 02118
SSNSocial Security Numbers and national IDs123-45-6789
DATE_OF_BIRTHDates of birth (in context)born January 14, 1985
CREDIT_CARDCredit/debit card numbers4111 1111 1111 1111
IP_ADDRESSIPv4 and IPv6 addresses192.168.1.1
URLPersonal URLs, profile linkslinkedin.com/in/johndoe

Rate limits

PlanSingle redactBatch redactMax chars
Free (no key)3/dayNot available2,000
Redact Pack ($9)50 totalUp to 5 docs10,000
Unlimited ($19/mo)UnlimitedUp to 20 docs10,000

Error codes

HTTP statusCodeMeaning
400TEXT_TOO_LONGText exceeds the character limit for your plan.
401INVALID_LICENSELicense key not found or expired.
402QUOTA_EXHAUSTEDAll credits in a Redact Pack have been used.
429RATE_LIMITEDToo many requests. Free tier: 3/day. Back off and retry after the Retry-After header value.
500UPSTREAM_ERRORUpstream 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.

See pricing and get started →

Keep reading