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 of these types 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, organizations and locations / street addresses need a named-entity model rather than a
checksum, so they are not detected by default. To find and redact them in the same call, list
person, organization or location in types explicitly — see
Named-entity PII (opt-in) below. A request that does not list them is
byte-for-byte the deterministic endpoint described above: no model call, no added latency, no added
billing. For named entities beyond those three types (dates, money amounts, products…) without
redaction, use the NER API, which shares 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.
Named-entity PII (opt-in)
Listing person, organization or location in types adds LLM-backed named-entity detection to
the same request, so one call redacts both the checksum-validated identifiers and the names around
them:
curl -X POST https://api.sapling.ai/api/v1/pii \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text":"Ask Jane Smith (jane@acme.com) about Acme Corp in Paris.", "types": ["email", "person", "organization", "location"], "redact": true}'
{
"entities": [
{"type": "person", "text": "Jane Smith", "start": 4, "end": 14, "replacement": "[PERSON]"},
{"type": "email", "text": "jane@acme.com", "start": 16, "end": 29, "replacement": "[EMAIL]"},
{"type": "organization", "text": "Acme Corp", "start": 37, "end": 46, "replacement": "[ORGANIZATION]"},
{"type": "location", "text": "Paris", "start": 50, "end": 55, "replacement": "[LOCATION]"}
],
"flagged": true,
"types": ["email", "location", "organization", "person"],
"redacted_text": "Ask [PERSON] ([EMAIL]) about [ORGANIZATION] in [LOCATION]."
}
The named-entity types are strictly opt-in and behave differently from the deterministic ones:
- Only when listed. They never run by default; a request without an explicit
typeslist, or with a list that omits them, stays fully deterministic. - Length cap. A request that lists any of them is capped at 10,000 characters (the NER API's cap) rather than 20,000, because the whole text is analyzed by the model in one pass.
- Billing. On top of the per-character PII rate, the request bills one NER-priced unit for the
submitted characters — see Pricing. The recognition shares its cache and its
3-day billing identity with
/api/v1/ner, so submitting the same text and named-entity types through either endpoint is billed as one unit of recognition work, not two. - Failures are loud. If the model call fails, the whole request returns a
502and nothing is billed or cached — a partial response missing the requested name spans would read as "no such PII here", which is exactly the silent leak a redaction pipeline cannot tolerate. Retry rather than changing the request. - Trial rate limit. On trial keys, cache-missing model calls are rate-limited; when exhausted
the request returns a
429with aRetry-Afterheader (in seconds). The allowance is shared with/api/v1/ner. - Overlaps. If a model-reported span overlaps a deterministic detection, the deterministic span wins — it is checksum-validated against the exact characters it covers (the model may, for example, report an email address as an organization mention), and the higher-precision placeholder is kept.
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, or 10,000 when types includes a
named-entity type. 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. Text containing a NUL character
(U+0000) is rejected with a 400 (it cannot be stripped without shifting every later offset, and
never occurs in legitimate text).
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 |
person | Person names — opt-in, never run by default | LLM-backed named-entity recognition |
organization | Companies, agencies, institutions — opt-in | LLM-backed named-entity recognition |
location | Locations and street addresses — opt-in | LLM-backed named-entity recognition |
The last three run only when listed explicitly; "defaults to all types" means all deterministic types.
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],[PERSON],[ORGANIZATION],[LOCATION].
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 of the deterministic types 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.