/v1/profanity
Scans free text for profane language, per the language it's written in.
- METHOD
- POST
- BODY CAP
- 16 KiB
- CHECKS
- 3
- FINDING CODES
- 2
POST /v1/profanity scans a block of free text — a comment, a review, a chat message —
for profane language before you publish or forward it.
Request#
{ "text": "this is complete sh1t honestly", "languages": ["en"] }
| Field | Type | Required | Notes |
|---|---|---|---|
text |
string | yes | Bounded only by this gate’s 16 KB request-body cap (D-80) — no separate limit here. That’s roughly 15,000 characters of English text; fewer for text with accents or emoji. |
languages |
array of strings | no | ISO 639-1 codes, up to 10. The languages your users write in — see “Which list applies” below. Norwegian: send "no", "nb" or "nn", all three are accepted and treated the same. |
redact |
string | no | Only "mask" is accepted; anything else is a 400 invalid_request. |
Checks this gate runs#
| Check | What it does |
|---|---|
profanity.word |
Matches text against the applicable language’s profanity word/phrase list — see profanity.word for exactly how matching and normalisation work. |
text.language |
Identifies the dominant language, when confident enough to act on. Producer only — emits no finding of its own. |
text.unexpected_language |
Fires when the text is confidently written in a language outside your declared languages — see text.unexpected_language. Only runs when you send languages. |
All three are pure rule-based text scanning and a compiled-in trigram language detector — no model call, no network — so all three are free on every plan (design rule 4).
Which list applies#
- Your
languages, if you send it. - Otherwise, the language
text.languagedetects with confidence. - Otherwise, English.
Never every list at once: an English-plus-everything filter flags ordinary Swedish words
like slut (“end”) and fan (“damn”/informally “fan of”) as profanity, because they
happen to collide with English vocabulary. Sending languages is the single most
effective thing you can do with this gate — it fixes both false positives (the wrong
list applied) and false negatives (short text the detector can’t confidently classify
falls back to English and never gets checked against the right list at all).
What this gate does not catch#
- Compound words. Whole-word matching is what keeps place names like Scunthorpe, Penistone and Clitheroe clean — the cost is that a compound like Swedish or German profanity glued onto another word (“skitbra”) isn’t matched, because it’s one token, not two. Accepted tradeoff (D-79) — see profanity.word.
- Words in a language with no list. Twelve languages today:
en sv no da fi de nl fr es it pt pl. Text in any other language is never checked, silently. - Slurs as a distinct category. Every match is
profanity.wordtoday; a higher-severity code for slurs specifically waits on a categorised source list. - Free-text names, addresses or other personal data. That’s
/v1/pii.
Word lists#
Seeded from LDNOOBW (CC-BY-4.0), with curated per-language removals for words that are mostly innocent in ordinary use — verified against a corpus of real place names, surnames and everyday words a naive filter breaks on. A removal is a diff, never a silent hand-edit, and a few words stay on the list anyway as documented, accepted collisions (an English given name that’s also a list entry; a village whose real name is a profanity) rather than being quietly dropped.
Example#
curl -s -X POST https://api.stormdoor.dev/v1/profanity \
-H "Authorization: Bearer sk_live_..." \
-H "content-type: application/json" \
-d '{"text":"this is complete sh1t honestly","languages":["en"]}'
{
"ok": true,
"verdict": "suspect",
"findings": [
{ "code": "profanity.word", "severity": "medium", "message": "profane language detected", "location": "18-22" }
],
"data": {
"matches": [
{ "code": "profanity.word", "rule": "en", "start": 18, "end": 22 }
]
},
"checks_run": ["profanity.word", "text.language", "text.unexpected_language"],
"checks_skipped": [],
"meta": { "gate": "profanity", "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.