Sensitive Data Handling

Ma Mahalakshmi V Updated 19 Sep 2026
7 min read

Handling sensitive data in AI workflows

Sensitive data — personally identifiable information (PII) such as names, email addresses, phone numbers, government ID numbers, and payment details — flows through AI applications constantly: a customer support assistant reads a customer's account details, a document-summarization tool processes contracts full of names and addresses, a data-analysis assistant queries a database of user records. This lesson covers the specific risks that arise when sensitive data reaches a prompt, and the concrete techniques for reducing exposure without breaking the functionality that needs the data in the first place.

Why sending raw PII to a model call is a distinct risk

Every request you send through the API leaves your own infrastructure and is processed by OpenAI's systems. That is true of any third-party API call, but it matters specifically for PII because of a few compounding factors:

  • Data minimization principles — found in regulations like GDPR and CCPA, and simply good practice regardless of jurisdiction — hold that you should only transmit and retain the minimum personal data necessary for a given purpose. Sending a customer's full record when only their subscription tier is relevant violates this principle even if the transmission itself is otherwise secure.
  • Retention and processing terms differ from your own systems. Your own database has retention and access policies you control directly. A third-party API call is governed by that provider's data usage policies, which may differ from what you need for compliance with a specific regulation or a specific customer contract.
  • Prompts and completions can end up in logs (yours, and potentially the provider's, depending on API tier and settings) — which multiplies the number of places sensitive data physically exists, and therefore the number of places it could leak from.
  • The model may echo sensitive data back into its output, which then flows into whatever your application does with that output — displaying it, storing it, logging it — extending exposure to systems that had no reason to touch the raw data at all.

None of this means you can never process PII through the API — plenty of legitimate applications need to. It means you should default to sending the least sensitive representation of the data that still lets the model do its job.

Redaction before the prompt is built

The most direct technique is redacting or masking sensitive substrings before they ever enter the prompt, and — where the workflow requires it — reinserting the real values afterward using a mapping your own code controls, so the model itself never sees the real value.

import re

REDACTION_PATTERNS = {
    "EMAIL": re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"),
    "PHONE": re.compile(r"\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b"),
    "SSN": re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
    "CREDIT_CARD": re.compile(r"\b(?:\d{4}[-\s]?){3}\d{4}\b"),
}


def redact(text: str) -> tuple[str, dict[str, str]]:
    """
    Replaces sensitive substrings with placeholder tokens.
    Returns the redacted text and a mapping of token -> original value,
    so the caller can restore the real values later if needed.
    """
    mapping: dict[str, str] = {}
    counter = 0
    redacted = text

    for label, pattern in REDACTION_PATTERNS.items():
        def replace_match(match, label=label):
            nonlocal counter
            counter += 1
            token = f"[{label}_{counter}]"
            mapping[token] = match.group(0)
            return token

        redacted = pattern.sub(replace_match, redacted)

    return redacted, mapping


def restore(text: str, mapping: dict[str, str]) -> str:
    """Reinserts original values for any placeholder tokens still present."""
    restored = text
    for token, original in mapping.items():
        restored = restored.replace(token, original)
    return restored

redact scans the input text against a set of known PII patterns and replaces each match with a labeled placeholder token ([EMAIL_1], [PHONE_1], and so on), while remembering the real value behind each token in mapping. The model then sees only the redacted version — it can reason about the structure of the text ("this customer provided their email and phone number") without ever seeing the actual email or phone number. If your workflow needs the real values back in the final output (for example, generating a formatted letter that must contain the actual customer email), restore reverses the substitution using the mapping your own code held onto the whole time — the model never needed the real value to do its part of the job.

def test_redact_masks_email_and_phone():
    text = "Contact John at john.doe@example.com or 555-123-4567."
    redacted, mapping = redact(text)
    assert "john.doe@example.com" not in redacted
    assert "555-123-4567" not in redacted
    assert "[EMAIL_1]" in redacted
    assert "[PHONE_1]" in redacted
    print("PASS: redact masks email and phone substrings")


def test_restore_reverses_redaction():
    text = "Reach out to jane@example.com about the account."
    redacted, mapping = redact(text)
    restored = restore(redacted, mapping)
    assert restored == text
    print("PASS: restore reconstructs the original text from the mapping")


test_redact_masks_email_and_phone()
test_restore_reverses_redaction()

Using structured outputs to avoid unnecessary echoing

A second, complementary technique addresses a different part of the problem: even when the model legitimately needs some sensitive input (say, verifying that a provided email matches an account on file), you don't want the model repeating that sensitive value back inside free-form prose in its output, because free-form output is harder to control and more likely to end up displayed or logged somewhere unintended. Constraining the model to a structured response — as covered elsewhere in this course for structured outputs generally — lets you define a schema that simply doesn't include a field for echoing raw PII back.

from pydantic import BaseModel


class AccountLookupResult(BaseModel):
    account_found: bool
    subscription_tier: str | None
    # Deliberately no field for echoing the customer's email or phone back —
    # the caller already has that data and doesn't need the model to repeat it.


def build_lookup_request(client, redacted_customer_text: str):
    return client.responses.parse(
        model="gpt-5.6-terra",
        input=(
            "Given the following redacted customer message, determine "
            "whether it looks like a request for account status, and "
            f"nothing else:\n\n{redacted_customer_text}"
        ),
        text_format=AccountLookupResult,
    )

By constraining the output shape with text_format, you remove the opportunity for the model to include a raw PII value in its response, because the schema has nowhere for it to go. This is a stronger guarantee than a prompt instruction like "don't repeat the customer's email," because it's enforced by the response's structure rather than by the model choosing to comply.

Note: Redaction patterns like the ones above are a practical baseline, not a formally complete PII detector. Regexes will miss unusual formats (international phone numbers, names, addresses) and can occasionally over-match. For applications with strict compliance requirements, pair this approach with a dedicated PII-detection library or service, and treat the regex-based approach shown here as one layer of a broader data-handling strategy rather than a complete solution by itself.

Deciding what actually needs to reach the model

Before reaching for redaction, ask a more basic question: does the model need the sensitive field at all? Many workflows send an entire customer record to the model when only one or two fields are relevant to the task. Trimming the payload down to exactly what's needed is a simpler and more reliable form of data minimization than redacting a large blob after the fact.

def build_minimal_context(customer_record: dict, task: str) -> dict:
    """Extracts only the fields relevant to a given task, dropping the rest."""
    field_requirements = {
        "shipping_status": ["order_id", "shipping_status", "estimated_delivery"],
        "subscription_info": ["subscription_tier", "renewal_date"],
    }
    needed_fields = field_requirements.get(task, [])
    return {k: v for k, v in customer_record.items() if k in needed_fields}

For a shipping_status task, this function drops the customer's name, email, payment details, and anything else not explicitly listed — none of it was ever going to be sent to the model in the first place, which is strictly safer than sending everything and redacting afterward, since data that was never transmitted cannot leak from the transmission.

Common Mistakes

  • Redacting only the "obvious" fields and missing formats your regex doesn't cover. International phone numbers, alternate SSN formats, and names are notoriously hard to catch with simple patterns — treat regex redaction as a baseline, not a guarantee, especially for regulated data.
  • Sending an entire database record when only one field is relevant. This is the most common and most avoidable source of unnecessary PII exposure — trimming the payload (as in build_minimal_context) is often more effective than redaction after the fact.
  • Letting the model's free-form output become the only place sensitive values are checked or handled, instead of constraining output structure so there's no field for that data to occupy in the first place.

Best Practices

  • Apply data minimization first: send only the fields a given task genuinely requires, before considering redaction of what remains.
  • Redact known PII patterns before constructing the prompt, and keep the real-value mapping in your own code rather than ever exposing it to the model.
  • Use structured output schemas to prevent the model from echoing sensitive values in free-form text, rather than relying solely on prompt instructions.
  • Treat regex-based redaction as one layer of defense, and use a dedicated PII-detection tool for applications with real compliance obligations.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.out Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Mem Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources
Ask about this post
AI Ask about this post

Ask questions about Sensitive Data Handling and get answers drawn from it.

Signed-in readers only.