docs
stormdoor.dev
GATE

/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#

  1. Your languages, if you send it.
  2. Otherwise, the language text.language detects with confidence.
  3. 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.word today; 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.

profanity.wordmediumprofanity.wordsuspect
text.unexpected_languagelowtext.unexpected_languagesuspect