PII Detection & Redaction API
The PII endpoint finds personally identifiable information in a text — email addresses, phone numbers,
US Social Security numbers, credit card numbers, IP addresses, IBANs and US bank routing numbers — and
optionally returns a redacted copy of the text with each entity replaced by a placeholder such as [EMAIL].
Detection is deterministic: every candidate must pass a structural check (for example a Luhn checksum for card numbers, a mod-97 checksum for IBANs, and the ABA checksum for routing numbers), so precision stays high without a machine-learning model, and results are fully reproducible. Every entity comes with exact character offsets into the text you sent, so you can apply the redactions to your own copy of a document — a support transcript, a chat log, an LLM prompt — before storing it or sending it on.
Person names and street addresses are not detected by this endpoint; those need a named-entity model rather than a checksum. For them, use the NER API, which returns people, organizations, locations and other named entities with the same offsets contract.
Sample Code
- cURL
- JavaScript
- Python
- Python (Sapling client)
curl -X POST https://api.sapling.ai/api/v1/pii \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text":"Reach me at jane.doe@example.com or (415) 555-0132.", "redact": true}'
import axios from 'axios';
async function run(text) {
try {
const response = await axios.post(
'https://api.sapling.ai/api/v1/pii',
{
key: '<api-key>',
text,
redact: true,
},
);
const {status, data} = response;
console.log({status});
console.log(JSON.stringify(data, null, 4));
} catch (err) {
const { msg } = err.response.data;
console.log({err: msg});
}
}
run('Reach me at jane.doe@example.com or (415) 555-0132.');
import requests
from pprint import pprint
response = requests.post(
"https://api.sapling.ai/api/v1/pii",
json={
"key": "<api-key>",
"text": "Reach me at jane.doe@example.com or (415) 555-0132.",
"redact": True,
}
)
if 200 <= response.status_code < 300:
pprint(response.json())
else:
print('Error: ', response.status_code, response.text)
from sapling import SaplingClient
api_key = '<api-key>'
client = SaplingClient(api_key=api_key)
result = client.pii(
'Reach me at jane.doe@example.com or (415) 555-0132.',
redact=True,
)
for entity in result['entities']:
print(entity['type'], entity['start'], entity['end'], entity['text'])
print(result['redacted_text'])
Sample Response
{
"entities": [
{
"type": "email",
"text": "jane.doe@example.com",
"start": 12,
"end": 32,
"replacement": "[EMAIL]"
},
{
"type": "phone",
"text": "(415) 555-0132",
"start": 36,
"end": 50,
"replacement": "[PHONE]"
}
],
"flagged": true,
"types": ["email", "phone"],
"redacted_text": "Reach me at [EMAIL] or [PHONE]."
}
Without redact, the response contains only entities, flagged and types.
Request Parameters
POST to https://api.sapling.ai/api/v1/pii
key: String
32-character API key. Can also be supplied via the Authorization header as a bearer token; if both are provided, the key parameter takes precedence.
text: String
Text to scan for PII. Up to 20,000 characters. The text is scanned exactly as sent — HTML is not
stripped and whitespace is not normalized — so that the returned offsets line up with your original.
Pass plain text rather than HTML markup for the best results.
types: List
Optional list of PII types to detect. Defaults to all types. Supported values:
| Type | What it matches | Validation |
|---|---|---|
email | Email addresses | Address format |
phone | Phone numbers, national or international (+) format, with common separators | Digit count and separator shape; dates and SSN-shaped numbers are rejected |
ssn | US Social Security numbers | 3-2-4 groups with a consistent separator, plus SSA area/group/serial rules |
credit_card | Payment card numbers (Visa, Mastercard, American Express, Discover, JCB, Diners Club, UnionPay, Maestro) | Luhn checksum and known issuer prefixes |
ip_address | IPv4 and IPv6 addresses | Parsed as an IP address; version strings and times are rejected |
iban | International Bank Account Numbers | Mod-97 checksum and per-country length |
us_bank_routing | US ABA bank routing numbers | ABA checksum and Federal Reserve prefix ranges |
redact: Boolean
If true, the response also includes redacted_text: the input with every detected entity replaced by
its replacement placeholder. Defaults to false.
Response Parameters
entities: List
The detected PII entities, in document order. Spans never overlap; where two candidate matches overlap,
the checksum-validated type wins (a Luhn-valid card number is never also reported as a phone number).
Each item has:
type: one of the types listed above.text: the matched text, exactly as it appears in the input.start,end: character offsets into the input text, such thattext[start:end]is the entity.replacement: the placeholder used for this type inredacted_text, e.g.[EMAIL],[PHONE],[SSN],[CREDIT_CARD],[IP_ADDRESS],[IBAN],[US_BANK_ROUTING].
flagged: Boolean
true if at least one entity was detected.
types: List
The sorted list of distinct PII types found in the text.
redacted_text: String
Only present when redact is true. The input text with each entity replaced by its placeholder.
Because the offsets refer to the original text, you can also build your own redaction (for example
masking all but the last four digits of a card number) by splicing entities into your copy of the text.
Notes
- Offsets count Unicode code points (Python
strindexing), not UTF-16 code units or bytes. In JavaScript, useArray.from(text)if the input may contain characters outside the Basic Multilingual Plane. - Because detection is pattern-based, a syntactically valid but fictitious value (a test card number, a made-up email address) is still reported; the endpoint answers "does this look like PII", not "does this identify a real person".
- For guardrail patterns around LLM inputs and outputs — including secrets and profanity — see Guardrails; for token-level profanity flagging see the Profanity API.