Verify masking in 60 seconds

Rendered from docs/dev-tools/verify.md. Documents are rendered as written in the repository.

Configuration that looks right is not evidence. This recipe proves, from three independent places, that a request went through zer0pii and that personal data in it was masked before it left.

1. Send a request with fake personal data

Use your configured tool, or curl against the same route the tool uses. The fake values below are deliberately non-real.

OpenAI-shaped route:

curl -sS -D headers.txt https://api.zer0pii.com/v1/chat/completions \
  -H "Authorization: Bearer zpii_..." \
  -H "X-zer0pii-Upstream-Key: sk-..." \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Repeat exactly: contact dana.levi@example.com or +972-52-000-0000"}]}'

Anthropic-shaped route (what Claude Code uses):

curl -sS -D headers.txt https://api.zer0pii.com/v1/messages \
  -H "Authorization: Bearer zpii_..." \
  -H "X-zer0pii-Upstream-Key: sk-ant-..." \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-5","max_tokens":100,"messages":[{"role":"user","content":"Repeat exactly: contact dana.levi@example.com or +972-52-000-0000"}]}'

Self-hosted: replace the hostname. If your admin configured the provider key on the gateway, drop the X-zer0pii-Upstream-Key line.

Expected reply body: the model repeats the email and phone as you sent them. That is the reconstitution working. The provider never saw them; it saw two masked tokens and echoed them back, and the gateway swapped the originals back in.

2. Read the response headers

headers.txt (or your tool's network log) carries these on every proxied response (source: apps/gateway/api/v1/proxy.py, zheaders):

Header What to look for
X-zer0pii-Redacted-Count 2 for the request above (one email, one phone). 0 means nothing was masked: check the count before anything else.
X-zer0pii-Mode standard. degraded-deterministic means the contextual name detector was skipped and only pattern-based detectors ran; emails and phones are still caught in that mode, free-text names may not be.
X-zer0pii-Request-ID req_ plus 12 hex characters. Copy it; it is the join key for the audit log.
X-zer0pii-Attestation The signed receipt, see step 4.
X-zer0pii-Latency total=...ms; scan=...ms; vault=...ms, the gateway's own overhead, excluding the provider.

If the headers are absent, the request did not go through zer0pii. The most common cause in Claude Code is a saved claude.ai login still being the active credential; /status shows it (see claude-code.md).

3. Confirm in the console

Sign in to the console.

The audit row is metadata written by the gateway at forward time (source: proxy.py, record_audit_event_later); it holds no byte of the payload, which is why you can look at it without a second privacy review.

4. Read the attestation receipt

The X-zer0pii-Attestation header has this exact shape (source: apps/gateway/core/attestation.py, Attestor.sign):

t=<unix seconds>;kid=<key id>;raw=sha256:<hex>;sanitized=sha256:<hex>;status=purged;sig=ed25519:<base64url>

To verify independently, fetch the public keys from https://api.zer0pii.com/.well-known/jwks.json (source: apps/gateway/api/v1/health.py; the audit page links the same URL under Verify with), pick the key whose kid matches, and check the signature. apps/gateway/core/attestation.py contains a reference verify(header, public_key, request_id) function you can lift into your own tooling.

A receipt that verifies against a published key is your evidence, independent of anything the console displays, that the masked hash is what left the gateway.

If step 2 shows Redacted-Count: 0

In order of likelihood:

  1. The request did not reach zer0pii (no X-zer0pii-* headers at all). Fix the base URL or the credential variable.
  2. The tool wrapped the text in a field the gateway treats as code or configuration. Put the fake values in the user message body, as above, for the check.
  3. The values do not match a detector. The email and phone above do; if you substituted your own, use a plainly formatted email and an international phone number for the check.