Moderate Text
Screen text for harmful content, check it against an age rating, and screen for your own custom themes, all in one synchronous request.
/v1/moderate/textModerate Text Content
Returns per-category scores, an overall flagged verdict, a recommended action (allow / review / block), and your current usage. Requests are processed synchronously; there is no async job or status endpoint to poll.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
text | string | Required | The text to moderate. Must contain at least one non-whitespace character and be at most 10,240 bytes (UTF-8 encoded, so multi-byte characters count more than once). "content" is accepted as an alias; if both are sent, "text" wins. Example: "This is some text to moderate" |
categories | string[] | Optional | Which categories to check. Default: ["toxicity", "profanity", "harassment", "hate", "sexual", "violence", "self_harm"]. Also available: "appropriateness" (age-rating check, opt-in, see content_standard), "drugs_alcohol" (opt-in: flags lyrics that depict or celebrate drinking, being drunk, or drug use; figures of speech like "high on love" don't count; binary score 1 or 0, or 0.5 if the check failed so it fails closed to review) and "spam" (placeholder, always scores 0). Unknown names are ignored. Only the categories you request appear in the response. Example: ["toxicity", "sexual", "appropriateness"] |
threshold | number | Optional | Score at or above which a category is flagged, between 0 and 1. Default 0.5. Applies to every category. Values outside 0 to 1 return 400 invalid_threshold. Non-numeric values are ignored and the default is used. Example: 0.7 |
thresholds | object | Optional | Per-category thresholds that override threshold for the named categories. Keys are category names, values are numbers from 0 to 1. A value of 1.0 effectively turns a score-based category off (profanity, appropriateness and drugs_alcohol are yes/no checks that ignore thresholds; leave them out of categories instead). Anything else returns 400 invalid_thresholds. Using it switches on custom-mode actions (see Action Semantics). Example: { "sexual": 0.2, "violence": 0.5, "hate": 0.2, "harassment": 0.4, "self_harm": 0.2 } |
action_thresholds | object | Optional | The score levels used to pick the action: { review, block }, both numbers from 0 to 1 with review no greater than block. Default { review: 0.5, block: 0.8 }. Anything else returns 400 invalid_action_thresholds. Using it switches on custom-mode actions. Example: { "review": 0.4, "block": 0.9 } |
content_standard | string | Optional | Age rating used by the appropriateness category: "G", "PG", "PG-13", "R", "NC-17", or "NR". Default "PG-13". Only has an effect when "appropriateness" is in categories. "NR" (not rated) skips the check, so appropriateness always passes (score 0). Unrecognized values fall back to "PG-13" without an error. Example: "PG" |
themes | string[] | Optional | Free-text themes to screen for, such as ["occult", "gambling"]. Up to 10 themes, each a non-empty string of at most 100 characters (after trimming); anything else returns 400 invalid_themes. Each theme is judged independently: a hit flags the request and forces action "block". Results come back under a top-level themes key. Runs whatever categories you choose. To vet themes your users type before saving them, call POST /v1/themes/validate with { "text": "..." } or { "themes": [...] }; it returns { themes: [{ text, valid, reason? }] }, rejecting instructions, questions, and prompt-injection attempts (max 10 items, 100 chars each, one request). Example: ["occult", "gambling", "alcohol"] |
consensus | integer | Optional | How many times to run the appropriateness check: 1, 3, or 5. Default 1. The runs are combined according to consensus_mode. Any other value (even numbers, 0, more than 5, non-integers) is treated as 1. Has no effect unless "appropriateness" is requested and content_standard is not "NR". Still counts as one request. Example: 3 |
consensus_mode | string | Optional | How consensus runs combine. "majority" (default): the text passes if more than half of the runs pass. "unanimous": the text passes only if every run passes, so any failing run flags it. Any value other than "unanimous" is treated as "majority". Only matters when consensus is greater than 1. Example: "unanimous" |
explain | boolean | Optional | Default false. When true and drugs_alcohol is flagged, its entry adds reason and excerpt (the offending line, verbatim) from the same check, either of which may be left out. When true and the appropriateness category is flagged, one extra check re-reads the text against the same content standard rules and sets categories.appropriateness.explain_verdict. "violation": it found a line that breaks the rules, and the entry adds reason (a short label such as "drug reference") and excerpt (that line, quoted verbatim from your input); either may be left out if no reliable value came back. "no_clear_violation": it found no such line, so appropriateness stays flagged but its score drops to 0.5, which on its own gives action "review" instead of "block" (other categories and themes still block as usual); no reason or excerpt is given. If the extra check fails or times out, explain_verdict is left out and the original verdict stands. Non-boolean values return 400 invalid_explain. Example: true |
metadata | object | Optional | Free-form object. Accepted for forward compatibility, but currently not stored or echoed back. Example: { "post_id": "abc123" } |
Request Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer m8r_sk_your_key_here | Required |
Content-Type | application/json | Required |
Also available at https://moder8r.app/api/v1/moderate/text.
Request
curl -X POST https://api.moder8r.app/v1/moderate/text \
-H "Authorization: Bearer m8r_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"text": "Thanks for the help yesterday, the fix worked great!"
}'Response
{
"request_id": "mod_3f2b8c1e-9a4d-4c7e-b1f0-6d2a5e8c9b13",
"flagged": false,
"categories": {
"harassment": { "flagged": false, "score": 0.001, "threshold": 0.5 },
"hate": { "flagged": false, "score": 0, "threshold": 0.5 },
"self_harm": { "flagged": false, "score": 0, "threshold": 0.5 },
"sexual": { "flagged": false, "score": 0, "threshold": 0.5 },
"violence": { "flagged": false, "score": 0.001, "threshold": 0.5 },
"toxicity": { "flagged": false, "score": 0.001, "threshold": 0.5 },
"profanity": { "flagged": false, "score": 0, "threshold": 0.5 }
},
"action": "allow",
"sentiment": "positive",
"usage": {
"daily": 42,
"monthly": 318,
"limit": 1000,
"remaining": 682
}
}Response Fields
request_idUnique ID for this request, in the form mod_<uuid>. Also included on most error responses. Include it when contacting support.
flaggedtrue if any returned category is flagged or any theme matched.
categoriesOne entry per requested category, each with flagged (boolean), score (0 to 1, rounded to 3 decimals), and threshold (the threshold that was applied). profanity and appropriateness come from a yes/no classifier, so their score is always 0 or 1. With explain: true, a flagged appropriateness entry can also include explain_verdict ("violation" or "no_clear_violation") and, for a violation, reason and excerpt. With "no_clear_violation" its score is 0.5 instead of 1.
actionRecommended action: allow, review, or block. See Action semantics.
themesOnly present when the request included themes. Keyed by each theme string as you sent it (trimmed), with the same shape as a category: flagged, score (1 for a match, 0 otherwise), and threshold (always 0.5).
sentimentOverall tone: positive, negative, neutral, or unknown. Included when profanity or appropriateness was checked (profanity is in the default set). It is informational only and does not affect flagged or action.
usageCounts for your whole account (all of its API keys combined), including this request: daily (today, UTC), monthly (this calendar month, UTC), limit (your plan's monthly request limit), and remaining (limit - monthly, never below 0).
Action Semantics
action is computed after flagging, using thresholds of 0.5 (review) and 0.8 (block) (or your action_thresholds):
- If
flaggedisfalse, the action isallow. - Otherwise, take the highest
scoreacross all returned categories and theme results (flagged or not). - That score at or above 0.8 gives
block, at or above 0.5 givesreview, and anything lower givesallow.
Custom mode. When the request sends thresholds, action_thresholds, or the drugs_alcohol category, only flagged categories and themes count toward the highest score, and any flagged category gives at least review, even if its score is below the review level. So a category set to 1.0 can't raise the action. Requests without these fields work exactly as described above.
| Situation | Resulting action |
|---|---|
| Nothing flagged | allow |
| Profanity, appropriateness, or a theme matched (score 1) | block |
Appropriateness flagged, explain: true, and the re-check found no clear violation (explain_verdict: "no_clear_violation", score 0.5), with nothing else at 0.8 or more | review |
| Highest score between 0.5 and 0.8, e.g. harassment 0.62 | review |
| Highest score 0.8 or more | block |
You set threshold below 0.5 and the highest score is below 0.5 | allow (even though flagged is true) |
threshold controls flagged. The action thresholds can't be changed on this endpoint. If you need different cut-offs, use flagged and the raw score values directly.
Categories
| Category | Default | Score | What it detects |
|---|---|---|---|
toxicity | Yes | 0–1 | Highest of the harassment and hate signals (including threatening variants) |
profanity | Yes | 0 or 1 | Explicit swear words, slurs, or graphic sexual or violent wording (not innuendo) |
harassment | Yes | 0–1 | Harassing or threatening language aimed at someone |
hate | Yes | 0–1 | Hate speech targeting protected groups, including threats |
sexual | Yes | 0–1 | Sexual content, including content involving minors |
violence | Yes | 0–1 | Violent content, including graphic violence |
self_harm | Yes | 0–1 | Self-harm content, intent, or instructions |
appropriateness | No | 0 or 1 | Whether the text fits the requested content_standard (see below) |
spam | No | Always 0 | Placeholder, not yet implemented. Never flags. |
harassment, hate, sexual, violence, and self_harm each report the highest score among their sub-signals.
Content Standards (Appropriateness)
Add "appropriateness" to categories to check whether the text fits an age rating. content_standard picks the rating (default PG-13). The result is binary: score 1 and flagged: true when the text does not meet the standard, which also makes the action block.
| Standard | Passes |
|---|---|
G | All ages, e.g. a family event or school dance. Romance, crushes, heartbreak, dancing, and figures of speech pass. Fails on sexual content or innuendo, drug or alcohol use, profanity or slurs (including censored forms), or graphic violence or self-harm. |
PG | Mild romance without physical detail, casual slang, social and political commentary. Fails on sexual touching or clear innuendo, glorified drug or alcohol use, graphic violence, or strong profanity. |
PG-13 (default) | Suggestive themes, innuendo, mild profanity, and references to drinking or partying. Fails on graphic sexual description, strong profanity, graphic violence, explicit drug use, or hate speech. |
R | Strong language, explicit sexual themes, graphic violence, and drug use. Fails only on illegal acts against minors, credible incitement to violence, or content no mainstream platform would allow. |
NC-17 | Any legal adult content. Fails only on illegal content: sexual content involving minors, credible threats against real people, or hate speech inciting violence. |
NR | Not rated. The check is skipped and always passes (score 0, never flagged). |
The appropriateness classifier can disagree with itself on borderline text. Use consensus (3 or 5) to run it several times: by default the majority wins. With consensus_mode: "unanimous", any single failing run flags the text, which is the strictest setting. Consensus doesn't change your quota usage.
Theme Screening
themes lets you exclude subjects the standard categories don't cover, such as "gambling", "alcohol", "occult", or "heartbreak". For each theme, a classifier decides whether the text contains, references, depicts, invokes, or celebrates it, directly or through clear symbolism. It judges what the text is about, not incidental wording: "a magical evening" does not match "magic", but casting spells does.
- Input: an array of up to 10 strings, each 1 to 100 characters after trimming.
- Output: a top-level
themesobject keyed by your (trimmed) theme strings. Each entry hasflagged,score(1or0), andthreshold(0.5). - Any theme match sets
flagged: trueand the action toblock. Theme screening is meant for deliberate exclusion, not for nuanced review. - Themes are checked alongside whatever
categoriesyou request, in the same request and for the same single quota unit. - If the classifier returns an unreadable answer, themes are reported as not matched rather than failing the request.
Strict ("squeaky clean") screening
For a family-friendly or "squeaky clean" mode, combine a G or PG content standard, consensus: 3 (majority), and the themes your audience wants excluded:
curl -X POST https://api.moder8r.app/v1/moderate/text \
-H "Authorization: Bearer m8r_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"text": "Pour another round of whiskey, the night is young",
"categories": ["toxicity", "profanity", "sexual", "violence", "appropriateness"],
"content_standard": "G",
"consensus": 3,
"themes": ["alcohol", "gambling", "occult"]
}'{
"request_id": "mod_8d0c6f2a-41b7-4e59-9c3d-2a7e1f5b6c04",
"flagged": true,
"categories": {
"sexual": { "flagged": false, "score": 0, "threshold": 0.5 },
"violence": { "flagged": false, "score": 0.002, "threshold": 0.5 },
"toxicity": { "flagged": false, "score": 0.001, "threshold": 0.5 },
"profanity": { "flagged": false, "score": 0, "threshold": 0.5 },
"appropriateness": { "flagged": true, "score": 1, "threshold": 0.5 }
},
"action": "block",
"themes": {
"alcohol": { "flagged": true, "score": 1, "threshold": 0.5 },
"gambling": { "flagged": false, "score": 0, "threshold": 0.5 },
"occult": { "flagged": false, "score": 0, "threshold": 0.5 }
},
"sentiment": "positive",
"usage": {
"daily": 43,
"monthly": 319,
"limit": 1000,
"remaining": 681
}
}Response Headers
Successful responses and 429 rate_limit_exceeded responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix time in milliseconds). 429 responses also include Retry-After (seconds). See Rate Limits.
Errors
Requests are checked in this order: authentication (401), request body (400), rate limit (429), monthly quota (402), then moderation. Requests rejected before moderation don't count toward your quota, and requests rejected with a 400 don't count toward your rate limit either. Only 200 responses use quota.
| Status | Code | When |
|---|---|---|
| 400 | invalid_json | Body is not valid JSON |
| 400 | missing_text | No text (or content) field, or it isn't a string |
| 400 | empty_text | Text is empty or only whitespace |
| 400 | text_too_long | Text exceeds 10,240 bytes; details.max_bytes and details.current_bytes are included |
| 400 | invalid_threshold | threshold is outside 0 to 1 |
| 400 | invalid_thresholds | thresholds isn't an object mapping known category names to numbers from 0 to 1 |
| 400 | invalid_action_thresholds | action_thresholds isn't { review, block } with both from 0 to 1 and review <= block |
| 400 | invalid_themes | themes isn't an array of up to 10 non-empty strings of at most 100 characters; details.max_themes and details.max_theme_length are included |
| 400 | invalid_explain | explain isn't a boolean |
| 401 | missing_api_key | No Authorization header |
| 401 | invalid_auth_format | Authorization header doesn't start with "Bearer " |
| 401 | invalid_api_key | Key is unknown or revoked |
| 402 | quota_exceeded | Monthly limit reached; details.usage and details.limit are included |
| 429 | rate_limit_exceeded | Per-minute rate limit reached; see Retry-After |
| 429 | openai_rate_limit | Upstream moderation provider is rate limited; retry with backoff |
| 502 | moderation_failed | Upstream moderation provider returned an error; retry |
| 500 | internal_error | Unexpected server error |
See the Error Reference for response shapes and retry guidance.
Code Examples
JavaScript/Node.js
// Node.js / JavaScript
const response = await fetch('https://api.moder8r.app/v1/moderate/text', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MODER8R_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: 'Text to moderate',
categories: ['toxicity', 'profanity', 'sexual', 'violence', 'appropriateness'],
content_standard: 'PG',
}),
});
const result = await response.json();
if (!response.ok) {
throw new Error(`${result.error.code}: ${result.error.message}`);
}
if (result.action === 'block') {
// reject the content
} else if (result.action === 'review') {
// queue for a human
}
const flaggedCategories = Object.entries(result.categories)
.filter(([, c]) => c.flagged)
.map(([name]) => name);Python
# Python
import os
import requests
response = requests.post(
'https://api.moder8r.app/v1/moderate/text',
headers={
'Authorization': f"Bearer {os.environ['MODER8R_API_KEY']}",
'Content-Type': 'application/json',
},
json={
'text': 'Text to moderate',
'themes': ['gambling'],
},
)
result = response.json()
response.raise_for_status()
print(result['action']) # "allow" | "review" | "block"
print(result.get('themes', {})) # only present when themes were sent
print(result['usage']['remaining'])Legacy Endpoint: POST /v1/moderate
An older endpoint, POST /v1/moderate, is still available for existing integrations. New integrations should use /v1/moderate/text. The legacy endpoint doesn't support content_standard, themes, consensus, explain, sentiment, or the usage block. It differs in these ways:
- Body:
content(required, at most 10,000 characters) plus an optionaloptionsobject withcategories,threshold, andaction_thresholds(review, default 0.5;block, default 0.8). Unlike the text endpoint, this lets you change the action thresholds. - Response:
flagged,action,categories, andrequest_id. - Errors:
invalid_content,empty_content, andcontent_too_long(400); 402quota_exceededwitherror.usage.requests_month,error.usage.limit_month, anderror.upgrade_url; 429rate_limit_exceededwitherror.retry_after(seconds); 503moderation_rate_limitandmoderation_api_error. Error bodies don't include arequest_id. - Extra headers:
X-Quota-Remainingon every successful response, andX-Quota-Warningonce you've used 80% or more of your monthly quota.