/v1/pii
Scans free text for personal data — email addresses, phone numbers, IBANs, payment cards, SE/NO/FI/DK/DE/NL/US/GB national IDs, and US routing numbers.
- METHOD
- POST
- BODY CAP
- 64 KiB
- CHECKS
- 13
- FINDING CODES
- 13
POST /v1/pii scans a block of free text — a support ticket, a form field, anything you’re
about to store or forward — for personal data before it lands somewhere it shouldn’t.
Request#
{ "text": "Reach me at +46 70 123 45 67 or jane@example.com", "redact": "mask" }
| Field | Type | Required | Notes |
|---|---|---|---|
text |
string | yes | Bounded only by this gate’s 64 KB request-body cap (D-80) — no separate limit here. That’s roughly 60,000 characters of English text; fewer for text with accents or emoji. |
redact |
string | no | Only "mask" is accepted; anything else is a 400 invalid_request. See “Redaction” below. |
Checks this gate runs#
Every check here is pure rule-based text scanning — regexes and checksums over the text you sent, no network calls, no model inference — so all thirteen are free on every plan (CLAUDE.md design rule 4: “Never withhold a zero-marginal-cost check”).
| Check | What it does |
|---|---|
pii.email_address |
Free-text email addresses — see pii.email_address. |
pii.phone_number |
Phone numbers written with a + country code — see pii.phone_number. |
pii.iban |
IBANs, mod-97 checksum plus the issuing country’s fixed length — see pii.iban. |
pii.payment_card |
Card numbers, Luhn checksum plus a real issuer’s BIN prefix — see pii.payment_card. |
pii.national_id_se |
Swedish personnummer / samordningsnummer — see pii.national_id_se. |
pii.national_id_no |
Norwegian fødselsnummer / D-number — see pii.national_id_no. |
pii.national_id_fi |
Finnish henkilötunnus — see pii.national_id_fi. |
pii.national_id_dk |
Danish CPR number — see pii.national_id_dk. |
pii.national_id_de |
German Steuer-ID / IdNr — see pii.national_id_de. |
pii.national_id_nl |
Dutch BSN (burgerservicenummer) — see pii.national_id_nl. |
pii.national_id_us |
US Social Security number — see pii.national_id_us. |
pii.bank_routing_us |
US ABA routing number, next to a routing keyword — see pii.bank_routing_us. |
pii.national_id_gb |
UK National Insurance number — see pii.national_id_gb. |
What this gate does not cover#
- Free-text names, postal addresses, or health details. Recognising those reliably needs a model, not a regex — that’s a later paid check, and this gate doesn’t claim to catch them.
- Bare Danish, German, or Dutch numbers, a bare undashed US SSN, or a routing number with no keyword. CPR, Steuer-ID and BSN each have a weak or
absent checksum on their own (Denmark dropped its check digit in 2007; a random 9-digit
number passes the Dutch 11-test one time in 11), so those three checks also require a
recognisable keyword —
CPR,Steuer-ID/IdNr,BSN/burgerservicenummer,SSN/social securityorrouting/ABA/RTN— within about 40 characters of the number. A bare number with no such label nearby will not fire.
Overlapping matches#
If two checks match overlapping spans of the same text (rare, but possible — e.g. a payment card’s digits partially overlapping a phone number’s), only the more specific match is kept; the response never double-counts the same characters as two findings.
Redaction#
redact: "mask" adds data.redacted to the response: your input text with every match
replaced by *, one asterisk per character, so offsets still line up. It’s the only
redaction mode — see the response format docs for why hashing, tokenising
and removal are all cut rather than offered.
Matches, truncation and offsets#
Every match also appears in data.matches as { "code", "rule", "start", "end" } —
useful if you want to act on the pattern name without parsing a finding’s message.
start/end are Unicode code-point offsets, not bytes — the right choice for every
JS and Python caller, wrong only for code that indexes Go strings by byte. A scan is
capped at 100 matches; past that, data.truncated: true is set and only the first 100
(by position) are returned.
Example#
curl -s -X POST https://api.stormdoor.dev/v1/pii \
-H "Authorization: Bearer sk_live_..." \
-H "content-type: application/json" \
-d '{"text":"Contact jane@example.com or +46 70 123 45 67","redact":"mask"}'
{
"ok": true,
"verdict": "suspect",
"findings": [
{ "code": "pii.email_address", "severity": "low", "message": "email address detected", "location": "8-24" },
{ "code": "pii.phone_number", "severity": "low", "message": "phone number detected", "location": "28-47" }
],
"data": {
"matches": [
{ "code": "pii.email_address", "rule": "email_address", "start": 8, "end": 24 },
{ "code": "pii.phone_number", "rule": "phone_number", "start": 28, "end": 47 }
],
"redacted": "Contact ***************** or *******************"
},
"checks_run": ["pii.email_address", "pii.iban", "pii.national_id_de", "pii.national_id_dk", "pii.national_id_fi", "pii.national_id_no", "pii.national_id_nl", "pii.national_id_se", "pii.payment_card", "pii.phone_number"],
"checks_skipped": [],
"meta": { "gate": "pii", "ruleset": "reference-..." }
}
See response format for how checks_skipped reasons work, and don’t
proxy that array — or the raw match offsets — to your own end users.
Finding codes#
Each code links to its own page with what it looks at, why it matters and what to do.