API Documentation

Everything you need to integrate moder8r into your application

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.

POST
/v1/moderate/text

Moderate 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

NameTypeRequiredDescription
textstring
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"
categoriesstring[]
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"]
thresholdnumber
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
thresholdsobject
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_thresholdsobject
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_standardstring
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"
themesstring[]
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"]
consensusinteger
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_modestring
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"
explainboolean
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
metadataobject
Optional
Free-form object. Accepted for forward compatibility, but currently not stored or echoed back.
Example: { "post_id": "abc123" }

Request Headers

HeaderValueRequired
AuthorizationBearer m8r_sk_your_key_here
Required
Content-Typeapplication/json
Required

Also available at https://moder8r.app/api/v1/moderate/text.

Request

cURL 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

200 OK
{
  "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_id

Unique ID for this request, in the form mod_<uuid>. Also included on most error responses. Include it when contacting support.

flagged

true if any returned category is flagged or any theme matched.

categories

One 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.

action

Recommended action: allow, review, or block. See Action semantics.

themes

Only 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).

sentiment

Overall 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.

usage

Counts 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):

  1. If flagged is false, the action is allow.
  2. Otherwise, take the highest score across all returned categories and theme results (flagged or not).
  3. That score at or above 0.8 gives block, at or above 0.5 gives review, and anything lower gives allow.

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.

SituationResulting action
Nothing flaggedallow
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 morereview
Highest score between 0.5 and 0.8, e.g. harassment 0.62review
Highest score 0.8 or moreblock
You set threshold below 0.5 and the highest score is below 0.5allow (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

CategoryDefaultScoreWhat it detects
toxicityYes0–1Highest of the harassment and hate signals (including threatening variants)
profanityYes0 or 1Explicit swear words, slurs, or graphic sexual or violent wording (not innuendo)
harassmentYes0–1Harassing or threatening language aimed at someone
hateYes0–1Hate speech targeting protected groups, including threats
sexualYes0–1Sexual content, including content involving minors
violenceYes0–1Violent content, including graphic violence
self_harmYes0–1Self-harm content, intent, or instructions
appropriatenessNo0 or 1Whether the text fits the requested content_standard (see below)
spamNoAlways 0Placeholder, 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.

StandardPasses
GAll 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.
PGMild 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.
RStrong 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-17Any legal adult content. Fails only on illegal content: sexual content involving minors, credible threats against real people, or hate speech inciting violence.
NRNot 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 themes object keyed by your (trimmed) theme strings. Each entry has flagged, score (1 or 0), and threshold (0.5).
  • Any theme match sets flagged: true and the action to block. Theme screening is meant for deliberate exclusion, not for nuanced review.
  • Themes are checked alongside whatever categories you 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:

Strict 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": "Pour another round of whiskey, the night is young",
    "categories": ["toxicity", "profanity", "sexual", "violence", "appropriateness"],
    "content_standard": "G",
    "consensus": 3,
    "themes": ["alcohol", "gambling", "occult"]
  }'
200 OK
{
  "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.

StatusCodeWhen
400invalid_jsonBody is not valid JSON
400missing_textNo text (or content) field, or it isn't a string
400empty_textText is empty or only whitespace
400text_too_longText exceeds 10,240 bytes; details.max_bytes and details.current_bytes are included
400invalid_thresholdthreshold is outside 0 to 1
400invalid_thresholdsthresholds isn't an object mapping known category names to numbers from 0 to 1
400invalid_action_thresholdsaction_thresholds isn't { review, block } with both from 0 to 1 and review <= block
400invalid_themesthemes 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
400invalid_explainexplain isn't a boolean
401missing_api_keyNo Authorization header
401invalid_auth_formatAuthorization header doesn't start with "Bearer "
401invalid_api_keyKey is unknown or revoked
402quota_exceededMonthly limit reached; details.usage and details.limit are included
429rate_limit_exceededPer-minute rate limit reached; see Retry-After
429openai_rate_limitUpstream moderation provider is rate limited; retry with backoff
502moderation_failedUpstream moderation provider returned an error; retry
500internal_errorUnexpected 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 optional options object with categories, threshold, and action_thresholds (review, default 0.5; block, default 0.8). Unlike the text endpoint, this lets you change the action thresholds.
  • Response: flagged, action, categories, and request_id.
  • Errors: invalid_content, empty_content, and content_too_long (400); 402 quota_exceeded with error.usage.requests_month, error.usage.limit_month, and error.upgrade_url; 429 rate_limit_exceeded with error.retry_after (seconds); 503 moderation_rate_limit and moderation_api_error. Error bodies don't include a request_id.
  • Extra headers: X-Quota-Remaining on every successful response, and X-Quota-Warning once you've used 80% or more of your monthly quota.