Skip to content

Examples

Short recipes to copy. Each client below talks to Privyx instead of the provider: only the base URL changes, and the client keeps its own API key, which Privyx relays. The examples assume a proxy on the default port:

privyx proxy --upstream https://api.openai.com        # for the OpenAI examples
privyx proxy --upstream https://api.anthropic.com     # for the Anthropic examples

For a guided walkthrough instead, see the tutorials.

OpenAI Python SDK

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1")  # the key still comes from OPENAI_API_KEY

reply = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Write a short greeting to alice@example.com"}],
)
print(reply.choices[0].message.content)

The provider receives <PRIVYX_EMAIL_1> in place of the address; the reply has the address back. Streaming (stream=True) is restored as it arrives.

Setting OPENAI_BASE_URL=http://localhost:8000/v1 has the same effect without touching the code.

Anthropic Python SDK

import anthropic

client = anthropic.Anthropic(base_url="http://localhost:8000")  # the key still comes from ANTHROPIC_API_KEY

reply = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=16000,
    messages=[{"role": "user", "content": "Write a short greeting to alice@example.com"}],
)
print(next(block.text for block in reply.content if block.type == "text"))

Or set ANTHROPIC_BASE_URL=http://localhost:8000.

Node

import OpenAI from "openai";

const client = new OpenAI({ baseURL: "http://localhost:8000/v1" }); // the key still comes from OPENAI_API_KEY

const reply = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Write a short greeting to alice@example.com" }],
});
console.log(reply.choices[0].message.content);

The Anthropic SDK for Node takes baseURL: "http://localhost:8000" in the same way. Streaming, tool calls, and the Responses API are in An app on the OpenAI or Anthropic SDK; LangChain, LlamaIndex, LiteLLM, and the Vercel AI SDK are under Integrations.

One session per user

By default every request gets its own session. To keep one mapping per user of your application, send an x-privyx-session header:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    default_headers={"x-privyx-session": "user-42"},
)

anthropic.Anthropic takes the same default_headers argument. Privyx strips the header before forwarding and returns the session id in the response's x-privyx-session header. A persistent vault keeps the mapping across restarts; see Sessions and vault.

curl

curl http://localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-4o-mini",
       "messages": [{"role": "user", "content": "Write a short greeting to alice@example.com"}]}'
curl http://localhost:8000/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "claude-opus-5-5", "max_tokens": 1024,
       "messages": [{"role": "user", "content": "Write a short greeting to alice@example.com"}]}'

Privyx adds the anthropic-version header when the request has none.

Coding tools

privyx run starts a proxy for the tool and points it there:

privyx run claude      # Claude Code
privyx run codex       # Codex
privyx run aider       # aider

For a long-running proxy that several terminals share, start it once and set the variable yourself:

privyx proxy -c privyx.yaml --upstream https://api.anthropic.com
ANTHROPIC_BASE_URL=http://localhost:8000 claude

On the command line

# What would be masked in this text, and what the provider would get
privyx detect --transform "DB_PASSWORD=hunter2, mail dana@acme.example"

# The same for a file
privyx detect --transform --stdin < app.log

# Mask a file and keep the mapping; restore the answer later
privyx mask --map map.json -i app.log -o app.masked.log
privyx unmask --map map.json -i answer.txt

# Only the messages of a JSON request
privyx mask --map map.json -i request.json --path '$.messages'

# A JSONL dataset through a pipe
cat tickets.jsonl | privyx mask --map map.json -f jsonl -i - -o tickets.masked.jsonl

# What happened lately, without any content
privyx audit stats --since 24h

Configuration recipes

These files ship in the repository's configs/ folder and in the Docker image under /app/configs.

Your own word lists

# Example: pseudonymize your own list of literal terms
#
# `terms` takes plain strings — Privyx escapes them, orders the longest first so
# a compound term wins over a substring of itself, and adds word boundaries.
# Matching is case-insensitive.  The entity name is yours to choose; it is the
# {type} in the emitted token, e.g. <PRIVYX_PERSON_1>.
#
# Every value below is a placeholder — replace it with your own.

detector:
  type: regex               # keeps the built-in patterns (personal data and secrets)

  terms:
    PERSON:       [ann, bob, annbob]
    ORGANIZATION: [acme, initech]
    URL:          ["https://git.internal.example/team"]
    PROJECT:      [bluebird]          # any entity name works

  # `terms` and `patterns` are merged, so hand-written regexes still work.
  patterns:
    EMPLOYEE_ID: "\\bEMP-\\d{6}\\b"

operator:
  type: pseudonym

vault:
  type: memory

# With `policy.type: strict`, every entity above would also need to appear in
# `policy.allowed` — the default policy passes everything through.
policy:
  type: default

No plain text in the vault

# Example: encrypt operator — no plaintext PII at rest in the vault
#
# The vault stores the AES-256-GCM ciphertext of each detected value (not the
# original) and Privyx decrypts on the way back.  Requires the `crypto` extra:
#   pip install privyx[crypto]
#
# Provide the key via the environment (preferred) rather than committing it:
#   export PRIVYX_ENCRYPT_KEY=$(openssl rand -hex 32)

operator:
  type: encrypt
  key: ""                # 64 hex chars; set via PRIVYX_ENCRYPT_KEY

vault:
  type: memory

A stricter deployment

# Privyx strict configuration
#
# Stricter defaults for higher-sensitivity deployments.

host: 127.0.0.1
port: 8000
log_level: info

# Empty → falls through to provider.base_url below.
upstream_url: ""

vault:
  type: sqlite           # persist sessions across restarts
  dsn: sqlite+aiosqlite:///privyx.db

# `regex` patterns are layered over the built-in set, so only the additions
# need listing here.
detector:
  type: regex
  patterns:
    DATE_OF_BIRTH: "\\b\\d{4}-\\d{2}-\\d{2}\\b"

policy:
  type: strict
  allowed: [EMAIL, PHONE, SSN, CREDIT_CARD, IP_ADDRESS, DATE_OF_BIRTH,
            API_KEY, JWT, PRIVATE_KEY, AUTH_TOKEN, URL_CREDENTIAL, SECRET]

operator:
  type: pseudonym

# Empty secret → per-session counters.  Set one (or PRIVYX_ANCHOR_SECRET) to
# make pseudonyms deterministic across sessions.
anchor:
  type: hmac
  secret: ""

provider:
  type: generic
  base_url: http://localhost:20128
  api_key: ""
  headers: {}

# Keep the PII-safe audit trail on for higher-sensitivity deployments.
audit:
  enabled: true
  # path defaults to privyx-audit.log (privyx run: ~/.local/state/privyx/audit.log)

Sessions that survive a restart

vault:
  type: sqlite               # needs privyx[sqlite]
  dsn: /var/lib/privyx/privyx.db
  ttl: 604800                # forget sessions idle for a week

session:
  strategy: conversation     # one session per conversation

Refuse what cannot be masked

proxy:
  passthrough_unknown: false   # 403 for embeddings and every other unrouted path

Names without a list

detector:
  - type: regex               # built-in patterns, your terms and patterns
  - type: presidio            # needs privyx[presidio] and a spaCy model
    language: en
    entities: [PERSON, LOCATION]

Using the engine from Python

The engine can run inside your own program, without the proxy. The scripts in examples/ show how, and the test suite runs each of them. Python library walks through them.

"""Build the engine from settings, the way `privyx proxy` does.

Instead of wiring a detector, a policy, an operator, and a vault by hand, load
the same configuration the CLI reads and let Privyx assemble them.
"""

from __future__ import annotations

import asyncio

from privyx.config.loader import load_config
from privyx.core.builder import build_engine


async def main() -> None:
    # The same layers as the CLI: built-in defaults, a YAML file (pass its
    # path as the first argument), PRIVYX_* environment variables, then `extra`.
    settings = load_config(extra={"detector": {"terms": {"PROJECT": ["bluebird"]}}})
    engine, close = await build_engine(settings)
    try:
        session = await engine.get_or_create_session()

        masked = await engine.transform("bluebird ships to dana@acme.example", session=session)
        print(masked.text)  # <PRIVYX_PROJECT_1> ships to <PRIVYX_EMAIL_2>

        reply = f"Noted: {masked.text}"  # stands in for a model's answer
        restored = await engine.restore(reply, session.session_id)
        print(restored.text)  # Noted: bluebird ships to dana@acme.example
    finally:
        await close()  # releases the vault


if __name__ == "__main__":
    asyncio.run(main())