zer0pii is a drop-in gateway in front of OpenAI, Anthropic and Gemini. Your code keeps its SDK; only the base URL and the key change. Personal data in the request is masked before it leaves the gateway, and the originals are put back in the reply.
1. Get a key
Sign in to the console, open API keys (/console/keys) and click Issue key. Keys start with zpii_ and are shown once.
2. Change the base URL
Point the SDK at the gateway and use the zer0pii key as the API key. Your provider key travels in the X-zer0pii-Upstream-Key header, or your admin configures it once on the gateway so developers never handle it.
Python (OpenAI SDK):
from openai import OpenAI
client = OpenAI(
base_url="https://api.zer0pii.com/v1",
api_key="zpii_...",
default_headers={"X-zer0pii-Upstream-Key": "sk-..."},
)
Node (OpenAI SDK):
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.zer0pii.com/v1",
apiKey: "zpii_...",
defaultHeaders: { "X-zer0pii-Upstream-Key": "sk-..." },
});
Self-hosted gateways use their own hostname in place of api.zer0pii.com.
3. Make the first call
reply = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Repeat exactly: contact dana.levi@example.com or +972-52-000-0000"}],
)
print(reply.choices[0].message.content)
The reply repeats the email and phone as you sent them. The provider never saw them: it saw two placeholder tags and echoed them back, and the gateway swapped the originals in before the reply reached you.
4. The routes
All three are served under the same /v1 prefix. Streaming works on all of them; tool calls and tool results are masked and restored too.
| Route | Shape | Forwards to (default) |
|---|---|---|
POST /v1/chat/completions |
OpenAI Chat Completions | https://api.openai.com/v1 |
POST /v1/messages |
Anthropic Messages | https://api.anthropic.com/v1 |
POST /v1/models/{model}:generateContent and :streamGenerateContent |
Google Gemini | https://generativelanguage.googleapis.com/v1beta |
5. Headers
Request headers you may send:
| Header | Meaning |
|---|---|
Authorization: Bearer zpii_... |
Your zer0pii key. Required. |
X-zer0pii-Upstream-Key |
Your provider key, forwarded to the provider and never stored. Omit it if your admin configured the key on the gateway. |
X-zer0pii-Upstream-URL |
Send this request to a different provider endpoint (public hostnames only). |
X-zer0pii-Conversation-ID |
Keeps placeholder tags stable across the turns of one conversation, so the provider's prompt cache keeps hitting. Without it, tags are stable per organisation. |
X-zer0pii-Policy-Group |
healthcare, clinical, finance or hr switch the request to fail-closed behaviour. |
Response headers on every proxied reply:
| Header | Meaning |
|---|---|
X-zer0pii-Mode |
standard: both detection tiers ran, the pattern-based tier and the contextual name detector. degraded-deterministic: the name detector was skipped for this request (over budget or unavailable) and only the pattern-based tier ran. Emails, phones, IDs, cards and keys are still caught in that mode; free-text names may not be. Treat it as the first thing to check when a name got through. |
X-zer0pii-Redacted-Count |
How many entities were masked in the request. 0 means nothing was masked. |
X-zer0pii-Request-ID |
The join key for the audit log in the console. |
X-zer0pii-Attestation |
A signed receipt over the hashes of the raw and the masked body, verifiable against /.well-known/jwks.json. |
X-zer0pii-Latency |
The gateway's own overhead, excluding the provider. |
6. Verify
Configuration that looks right is not evidence. The verify guide proves from three independent places (the reply, the headers, the console audit log) that a request went through zer0pii and that the personal data in it was masked.
What is not masked
Code is left alone by design: variable names, package names, code blocks and syntax are never redacted; only a value that looks like a credential is. See the entity catalog for what is detected and the dev tools pages for IDE and CLI setups, including which tools cannot be covered.