Skip to content

PII Redaction

Obstruo detects and replaces personally identifiable information (PII) with reversible placeholders before any prompt reaches a language model. The original text never leaves your control plane.

How it works

Every inbound request passes through two detection layers, in order:

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#1E1E26', 'primaryBorderColor': '#8B6FE8', 'primaryTextColor': '#ECEAF6', 'lineColor': '#8B6FE8', 'secondaryColor': '#16161C', 'tertiaryColor': '#16161C', 'mainBkg': '#1E1E26', 'nodeBorder': '#8B6FE8', 'clusterBkg': '#16161C', 'clusterBorder': '#2A2A34', 'titleColor': '#ECEAF6', 'edgeLabelBackground': '#0E0E11', 'fontFamily': 'ui-monospace, SF Mono, Menlo, monospace'}}}%%
flowchart TD
    A([User prompt]) --> B
    B --> C
    C --> D([Redacted prompt → LLM])

    B["Layer 1 - Regex\nEmails · phones · credit cards · SSNs · IPs"]
    C["Layer 2 - NER\nNames · organisations · locations"]

Detected entities are replaced with tagged placeholders before the prompt reaches the model:

What you send:  "My name is Anna Kowalski and my email is anna@example.com"
What LLM sees: "My name is [PERSON] and my email is [EMAIL_ADDRESS]"

After the LLM responds, Obstruo maps the placeholders back to the original values before returning the response to your application. Your code sees the original text throughout - the substitution is invisible at the application layer.

The audit log records which entity types were detected and replaced, but never stores the original PII values.

Configuring redaction

Go to Enforce → Redaction in the panel.

Regex layer

Toggle built-in patterns on or off:

Pattern Example match
Email address anna@example.com
Phone number (Polish) +48 123 456 789
Phone number (North American) (555) 123-4567
Credit card 4111 1111 1111 1111
Social security number (US) 123-45-6789
IP address (IPv4 / IPv6) 192.168.1.1
IBAN PL61 1090 1014 0000 0712 1981 2874
Polish bank account (NRB) 26-digit account number
PESEL (Polish national ID) 11-digit number
NIP (Polish tax ID) 10-digit number
REGON (business register number) REGON: 123456785
KRS (National Court Register) KRS: 0000123456
Person (initial + surname) J. Kowalski
Postal code (Polish / US) 00-950 / 90210
Date (ISO / Polish / English) 2026-07-01 / 01.07.2026 / January 15, 2026
URL https://example.com/page

Patterns are scoped by your project's primary language - country-specific formats (PESEL, NRB, US SSN, …) are offered where they make sense, so enabling everything everywhere doesn't inflate false positives.

You can add custom patterns using standard regular expressions. A custom rule can't reuse a built-in category (the editor will point you back to the toggle list instead). The Free plan includes up to 3 custom regex rules per project.

The Templates button on the Redaction page offers starter bundles for common compliance scenarios (e.g. General PII, GDPR). Applying one is additive - it pre-selects the relevant patterns without removing anything you've configured.

NER layer

The NER layer uses a named entity recognition model to detect free-text entities that regex cannot catch - person names, company names, locations, and similar. Enable or disable it per project.

Testing redaction

The Redaction page includes a Test panel. Paste sample text and click Test to see exactly which entities would be detected and replaced - without saving any changes or sending a real request.

Use this to validate your configuration before enabling it in production.

Redaction metadata in responses

Every response carries redaction metadata in the obstruo key. The shape differs between streaming and non-streaming.

Non-streaming

The obstruo.redaction block in the response body shows how many entities were detected and by which category:

response = client.chat.completions.create(
    model="obstruo-chat",
    messages=[{"role": "user", "content": "My email is anna@example.com"}],
)

redaction = response.model_extra.get("obstruo", {}).get("redaction", {})
print(redaction.get("total_entities"))  # 1
print(redaction.get("by_category"))     # {"EMAIL_ADDRESS": 1}

Streaming

For streaming responses, redaction state is available in the X-Obstruo-Pii-State response header:

Value Meaning
no-pii No PII was detected in the prompt
redacted One or more entities were replaced with placeholders
disabled Redaction is turned off for this project
audit-only The key is in Audit-only mode - the request was redacted and logged, but not forwarded to an LLM
passthrough The request hit a binary endpoint (image or audio) - these are proxied without redaction
stream = client.chat.completions.create(
    model="obstruo-chat",
    messages=[{"role": "user", "content": "My email is anna@example.com"}],
    stream=True,
)
pii_state = stream.response.headers.get("x-obstruo-pii-state")
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

Next steps