Redacting a PDF properly before you send it outside

Patterns catch what you can describe; regions catch what you can see. Why a black rectangle is not redaction, and the verification step to run afterwards.

Drawing a black box over text in a PDF editor does not remove the text — it draws a rectangle on top of it, and anyone who selects the page or runs a text extractor gets the words back. This has embarrassed governments. Infrai’s POST /v1/pdf/redact takes patterns to match content and regions to cover areas, and removes rather than conceals.

The verification step afterwards is what turns that claim into something you’ve checked.

Redact by pattern

curl -sS -X POST "https://api.infrai.cc/v1/pdf/redact" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "pdf": "https://files.example.com/contracts/4821.pdf",
    "patterns": [
      "[0-9]{3}-[0-9]{2}-[0-9]{4}",
      "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}",
      "\\b(?:4[0-9]{12}(?:[0-9]{3})?)\\b"
    ],
    "store": true
  }'
{
  "ok": true,
  "data": {
    "pdf_id": "pdf_2fVc8nRqLmT4xBzY",
    "url": "https://files.infrai.cc/pdf/2fVc8nRqLmT4xBzY.pdf",
    "size_bytes": 172032,
    "page_count": 6,
    "sha256": "9f2c41bd7a9e8c0b5d3e1f7a2b8d4c6e0a9f3b1d5c7e2a4f6b8d0c2e4a6f8b0d",
    "created_at": "2026-09-21T03:45:00Z",
    "retention_days": 7,
    "source": "redact"
  }
}

Patterns are the right tool for structured identifiers: national insurance numbers, card numbers, email addresses, anything with a shape. They’re the wrong tool for names, because names don’t have a shape — “Ada Lovelace” matches no pattern you’d want to apply to a whole document.

For those, regions covers coordinates you specify, which means you have to know where on the page the text is.

Verify rather than trust

The check that matters takes one call: parse the redacted document and look for what should be gone.

import os
import re

import requests

API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})

PATTERNS = {
    "ssn": r"[0-9]{3}-[0-9]{2}-[0-9]{4}",
    "email": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}",
    "card": r"\b(?:4[0-9]{12}(?:[0-9]{3})?)\b",
}


def redact(pdf: str, patterns: list[str]) -> dict:
    resp = SESSION.post(f"{API}/v1/pdf/redact",
                        json={"pdf": pdf, "patterns": patterns, "store": True}, timeout=180)
    body = resp.json()
    if not body.get("ok"):
        raise RuntimeError(body["error"]["code"])
    return body["data"]


def extracted_text(pdf_id: str) -> str:
    resp = SESSION.post(f"{API}/v1/pdf/parse", json={"pdf": pdf_id}, timeout=120)
    resp.raise_for_status()
    return "\n".join(resp.json()["data"].get("text_per_page") or [])


def redact_and_verify(pdf: str) -> dict:
    """Redact, then extract the text back out and assert the patterns are gone.
    Shipping a redacted document without this step means trusting a claim you
    could have checked in one call — and the failure is not recoverable once the
    file has left your control."""
    result = redact(pdf, list(PATTERNS.values()))
    text = extracted_text(result["pdf_id"])

    survivors = {name: len(re.findall(pattern, text)) for name, pattern in PATTERNS.items()}
    leaked = {name: n for name, n in survivors.items() if n}
    if leaked:
        raise RuntimeError(f"redaction incomplete, do not release: {leaked}")

    return {"pdf_id": result["pdf_id"], "url": result["url"], "sha256": result["sha256"],
            "pages": result["page_count"], "verified": True}


if __name__ == "__main__":
    print(redact_and_verify("https://files.example.com/contracts/4821.pdf"))

Raising rather than warning is the right behaviour. A redaction that didn’t fully work should stop the release, not annotate it.

What patterns won’t catch

Sensitive contentPattern works?Approach
Card, national ID, phoneyespatterns
Email addressyespatterns
A person’s namenoregions, or a model pass to locate it
A figure in a tablenoregions
An embedded image of a signaturenoregions
Text in a scanned page with no text layernoOCR first, then decide

That last row is the trap. A scanned document has no text to match, so a pattern-based redaction over it does nothing at all and returns success — run POST /v1/pdf/parse first, and if the text layer is empty, patterns are not the tool.

For names and unstructured content, one workable approach is to parse the text, pass it to POST /v1/chat/completions to identify the spans to remove, and use what comes back to drive your pattern list. That keeps the model advising rather than deciding, and the verification step still runs afterwards.

Metadata leaks too

A redacted body with an intact document title, author field and producer string is a partial job. POST /v1/pdf/parse returns metadata, so check it — a title like “Smith settlement draft 3” undoes careful page work.

The same applies to attachments and extracted images: POST /v1/pdf/extract_images will show you what pictures the document carries, which is worth doing on anything received from outside before you pass it on.

Limitations

There’s no visual confirmation in the response: you get a document and a hash, not a preview, so a regions redaction placed at the wrong coordinates removes the wrong content and reports success. Render a page to an image with POST /v1/pdf/convert to png and look at it during development.

Nor is there a redaction audit log inside the document — no record of what was removed, which some legal processes require. And this is text and region removal, not a document-understanding service: it won’t find sensitive content you haven’t described.

A dedicated e-discovery or document-AI platform does entity detection, review workflows and audit trails properly, and for regulated disclosure work that’s what you want rather than an endpoint. Even among document APIs, ILovePDF’s processing suite and Anvil’s workflow tooling cover more of the surrounding process than a single redact call does. What’s here instead is that the redaction, the verification parse, the archive with PUT /v1/storage/object/put/{bucket}/{key} and the POST /v1/errors/capture when verification fails are one credential and one GET /v1/account/usage — redaction bills per call, live in GET /v1/discovery/pdf.redact (verified 2026-09-21), with rates drifting downward as vendor contracts improve.

References

Browse more pdf developer guides