API docs
Base URL: https://profanity-api.mike-tusa.workers.dev. JSON in, JSON out. English word list (obscenity englishDataset) with leetspeak, look-alike character, repeated letter and spaced/dotted letter handling. This is a word filter, not AI moderation, and it does not understand context.
POST /v1/check
Request body (JSON, at most 16,384 bytes):
| Field | Type | Notes |
|---|---|---|
text | string, required | At most 10,240 bytes (UTF-8). |
censor | boolean, default true | When false, censored is null. |
allow | string[] | Words never to flag (up to 50, each ≤ 40 chars, letters/spaces/hyphens/apostrophes). Matched against the canonical term (e.g. "shit") and the matched text. Allow wins over deny. Allowing a canonical term turns off every match of it: allow: ["cum"] clears "Cumming, Georgia" but also all other uses of that word, so use it only for that case. |
deny | string[] | Extra words to flag on word boundaries, with the same leetspeak, look-alike and dotted-letter handling (same limits as allow). |
curl -s https://profanity-api.mike-tusa.workers.dev/v1/check -H 'content-type: application/json' -H 'x-api-key: pf_…' \
-d '{"text":"well f.u.c.k that sh1t, jerk","deny":["jerk"]}'
Response:
{
"ok": true,
"has_profanity": true,
"match_count": 3,
"terms": ["fuck", "shit", "jerk"],
"matches": [
{ "term": "fuck", "match": "f.u.c.k", "start": 5, "end": 12, "source": "dataset", "obfuscated": true },
{ "term": "shit", "match": "sh1t", "start": 18, "end": 22, "source": "dataset", "obfuscated": false },
{ "term": "jerk", "match": "jerk", "start": 24, "end": 28, "source": "deny", "obfuscated": false }
],
"censored": "well ******* that ****, ****",
"truncated": false,
"text_bytes": 28,
"engine": "obscenity@0.4.6 (englishDataset)",
"version": "0.1.4"
}
start is inclusive and end exclusive, in JavaScript string (UTF-16 code unit) indices of your original text, so text.slice(start, end) returns the match. The censored copy replaces each matched code unit with *, so it has the same length as the input and the same positions apply. obfuscated: true means the match was found after joining spaced or dotted single letters (e.g. f.u.c.k, s h i t) or skipping hidden characters (soft hyphen, zero-width and other invisible format characters, combining marks), or on a masked fu*k-style spelling.
The real response is compact JSON (shown indented here). matches lists at most 500 entries in text order; when there are more, truncated is true. match_count and terms always cover every match, and censored always censors all of them.
POST /v1/keys
Issues a free API key: 1,000 requests/day, 60/min, commercial use allowed. No email or sign-up needed. The key is shown once; we store only its SHA-256 hash. Up to 3 keys per IP per day (approximate: the counter is eventually consistent, so parallel requests can occasionally get one more). Lost a key or need it revoked? Email mike.tusa@gmail.com
curl -s -X POST https://profanity-api.mike-tusa.workers.dev/v1/keys -H 'content-type: application/json' -d '{}'
Send it as X-API-Key. GET /v1/usage with the header shows today's usage.
Limits and headers
- Without a key: 10 requests/min and 200/day per IP. With a free key: 60/min and 1,000/day.
- Responses carry
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(daily quota, reset at 00:00 UTC, Unix seconds),X-RateLimit-PolicyandX-API-Plan. A 429 includesRetry-After. - Body over 16,384 bytes:
413 body_too_large. Text over 10,240 bytes:413 text_too_large. - There is also a service-wide daily soft cap. If it's reached, you get
429 daily_cap_reacheduntil 00:00 UTC.
Errors
Errors look like {"ok":false,"error":{"code":"…","message":"…"}}. Codes: invalid_json, missing_text, invalid_censor, invalid_allow, invalid_deny (400); invalid_api_key, missing_api_key (401); cross_origin_forbidden (403); not_found (404); method_not_allowed (405); body_too_large, text_too_large (413); unsupported_media_type (415); rate_limited, ip_daily_quota, key_quota_exceeded, issuer_quota_exceeded, key_issue_limit, daily_cap_reached (429); internal_error (500); not_configured, unavailable (503).
Other endpoints
GET /health returns the service status and version. GET /privacy is the privacy note.
Limitations
- English only. It's a word list, so it doesn't judge context, intent or tone, and it can miss new slang.
- Not caught: letters split across lines, spaced letters followed by a word (
f u c king), most symbol-masked spellings (f**k,b*tch,s**t), phonetic spellings (phuck), deliberate misspellings and words outside the list. Standalonefu/FUis not flagged on purpose (kung fu, Fu Manchu); masked forms likefu*k,fu**ingandfu.ckare, but notfu ck(space), doubled separators (fu..ck,fu--ck) or a hidden character betweenfuand the mask (e.g. a zero-width space infu*k). - Caught: leetspeak (
sh1t,$h!t), look-alike and accented letters (fück,fuck), repeated letters (fuuuck), dotted/spaced letters (f.u.c.k,s h i t), dotted letters with a suffix (f.u.c.king,s.h.i.tty), and hidden characters (soft hyphens, zero-width and direction-control characters, other invisible format characters, combining and enclosing marks). - It can flag innocent words that contain a swear word. We whitelist the common ones (like Scunthorpe, cockpit, shiitake, Shiite, cumin, Fukushima, Dickinson). Known false positives: the first name
Dickand the place/surnameCumming(e.g. Cumming, Georgia) are still flagged; add"dick"or"cum"toallowif your text has them. Note thatallow: ["cum"]turns off all detection of that word (including its slang use), so only use it if place/surname text matters more to you. Rare edge cases also flagged:Fu-kien(old spelling of Fujian) andfu.kr-style domains. Useallowfor any others.
Support
Questions, key revocation or abuse reports: mike.tusa@gmail.com.
Pricing
Free
1,000 requests/day with a free API key (60/min). Commercial use allowed. No card, no sign-up form.
Without a key: 200/day and 10/min per IP.
Starter (coming soon)
About 100,000 requests/month.
Pro (coming soon)
About 1,000,000 requests/month, plus saved custom allow/deny lists.
Paid tier prices are estimates and not available to buy yet. Per-request allow/deny lists (up to 50 words each) work on every tier, including free.