Skip to navigation

Moderation API

Moderation API

Send one piece of text and get back a judgement you can act on: a score from 0 to 1 for each of seven categories, whether each one crossed its line, and a single verdict — allow, review or block. It is built for the checks around user-written and model-written text: a comment before it is published, a prompt before it reaches a model, a reply before a user sees it.

POST https://api.beatapi.io/v1/moderations

The request and response follow OpenAI’s Moderation API, so the official OpenAI SDKs work with only the base URL and the API key changed. This was verified on 2026-10-12 with the official Python SDK (openai 2.48.0).

ModelWhat it is
beat-moderation-1BeatAPI’s moderation model. $0.042 per 1M input tokens. model is optional in the request; every call is answered by this model.

A failed call judged nothing. Never treat an error — a timeout, a 4xx, a 503 — as “allowed”. Hold the content, or retry.

Quick start

curl https://api.beatapi.io/v1/moderations \
-H "Authorization: Bearer $BEATAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": "Forged ID cards and bank cards for sale, shipped within 24 hours. Message me for the price list."}'

The response below is a real one, returned for an advertisement of this kind that offered forged ID cards and bank cards.

{
"id": "modr-task_UnzjwhQHAITmqaLLJUi3ML5gQYnRfDln",
"model": "beat-moderation-1",
"results": [
{
"flagged": true,
"categories": {
"sexual": false,
"sexual/minors": false,
"violence": false,
"hate": false,
"harassment": false,
"self-harm": false,
"illicit": true,
"harassment/threatening": false,
"hate/threatening": false,
"illicit/violent": false,
"self-harm/intent": false,
"self-harm/instructions": false,
"violence/graphic": false
},
"category_scores": {
"sexual": 0,
"sexual/minors": 0.01,
"violence": 0.02,
"hate": 0.01,
"harassment": 0.02,
"self-harm": 0.01,
"illicit": 0.97,
"harassment/threatening": 0,
"hate/threatening": 0,
"illicit/violent": 0,
"self-harm/intent": 0,
"self-harm/instructions": 0,
"violence/graphic": 0
},
"category_applied_input_types": {
"sexual": ["text"],
"sexual/minors": ["text"],
"violence": ["text"],
"hate": ["text"],
"harassment": ["text"],
"self-harm": ["text"],
"illicit": ["text"],
"harassment/threatening": [],
"hate/threatening": [],
"illicit/violent": [],
"self-harm/intent": [],
"self-harm/instructions": [],
"violence/graphic": []
},
"verdict": "block",
"sexual_level": "none"
}
],
"policy_version": "2026-10-12",
"usage": { "input_tokens": 1206 }
}

illicit scored 0.97, above its line of 0.6, so categories.illicit is true, flagged is true and the verdict is block.

OpenAI SDK (Python)

Point the client at BeatAPI and call moderations.create as usual. The fields BeatAPI adds are readable on the typed result.

import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["BEATAPI_API_KEY"],
base_url="https://api.beatapi.io/v1",
)
resp = client.moderations.create(
input="Forged ID cards and bank cards for sale, shipped within 24 hours. Message me for the price list.",
)
result = resp.results[0]
# The values in the comments are the ones from the response above.
print(result.flagged) # True
print(result.verdict) # "block" (added by BeatAPI)
print(result.categories.illicit) # True
print(result.category_scores.illicit) # 0.97
print(result.sexual_level) # "none" (added by BeatAPI)
print(resp.policy_version) # "2026-10-12" (added by BeatAPI)

JavaScript

const text = "Forged ID cards and bank cards for sale, shipped within 24 hours. Message me for the price list.";
const response = await fetch("https://api.beatapi.io/v1/moderations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BEATAPI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ input: text }),
});
if (!response.ok) {
// A failed call judged nothing: do not let the text through.
// On HTTP 429, wait the Retry-After seconds before retrying.
throw new Error(`Moderation HTTP ${response.status}; Retry-After: ${response.headers.get("Retry-After")}`);
}
const moderation = await response.json();
const result = moderation.results?.[0];
if (!result || !["allow", "review", "block"].includes(result.verdict)) {
throw new Error("Invalid moderation response; hold the text for review");
}
if (result.verdict === "block") {
console.log("rejected", result.category_scores);
} else if (result.verdict === "review") {
console.log("queued for a person", result.sexual_level, result.category_scores);
} else {
console.log("allowed under policy", moderation.policy_version);
}

Request

FieldTypeRequiredDescription
inputstring | arrayYesThe one text to check: a string, or an array holding exactly one item — a string or {"type": "text", "text": "..."}. At most 8,000 characters.
modelstringNobeat-moderation-1, or one of OpenAI’s moderation model names (omni-moderation-latest, omni-moderation-2024-09-26, text-moderation-latest, text-moderation-stable), which are accepted and answered by beat-moderation-1. Any other name is refused with a 400.

Response

FieldDescription
idIdentifier of this call. Quote it in support tickets.
modelAlways beat-moderation-1, whichever accepted name the request used.
resultsAn array with one entry, because one text is judged per call.
results[].flaggedtrue when any category’s score reached its line.
results[].categoriesFor each category, whether its score reached its line.
results[].category_scoresFor each category, a score from 0 to 1.
results[].category_applied_input_types["text"] for the seven judged categories and [] for the six sub-categories.
results[].verdictallow, review or block. Added by BeatAPI.
results[].sexual_levelnone, contextual, suggestive or explicit. Added by BeatAPI.
policy_versionThe policy version that judged the text. Scores from different policy versions are not comparable. Added by BeatAPI.
usage.input_tokensThe input tokens this call counted. Added by BeatAPI.

The response is synchronous: a successful HTTP response carries the judgement. There is nothing to poll.

Categories

Seven categories are judged, named as OpenAI names them, each with a score from 0 to 1.

CategoryWhat it flagsWhat it deliberately does not flag
sexualExplicit sexual content, requests for it, erotic role-play, and offers or ads for sexual services or pornography.Medical, sex-education, news or other non-arousing mentions. Merely suggestive content is not flagged either; it gets the review verdict.
sexual/minorsAny sexualisation of a person under 18, however fictional or coded.Child-protection, reporting or age-appropriate education content.
violenceThreats, incitement, glorification and gory detail.Neutral news, history, sports, games or non-graphic fiction.
hateHatred of people for a protected characteristic: race, ethnicity, nationality or regional origin, religion, gender, sexual orientation, disability and the like.Quoting hate to criticise it, and criticism of ideas and governments.
harassmentInsults, intimidation, bullying, doxxing and pile-ons against a person or group.Civil disagreement, and blunt criticism of work or ideas.
self-harmPromoting or instructing self-harm, or the writer expressing a current intent.Prevention, support or recovery content.
illicitActionable help to commit a crime, and offers of or requests for illegal goods and services.News, law, harm reduction or security defence.

OpenAI’s six sub-category keys — harassment/threatening, hate/threatening, illicit/violent, self-harm/intent, self-harm/instructions and violence/graphic — are present so that typed SDKs keep working, but they are not judged separately. They are always false, with a score of 0 and an empty category_applied_input_types list. Do not build rules on them.

Lines and verdicts

Every judged category has a line: 0.6, or 0.3 for sexual/minors. categories[k] is the comparison of that category’s score with its line, and flagged is true when any category’s score reaches its line.

verdictWhenWhat to do
blockAt least one category’s score reached its line. It is the same as flagged: true.Reject the text.
reviewNo score reached its line, but one is within 0.25 below it and never below 0.15 — from 0.35 for most categories, from 0.15 for sexual/minors — or the text is sexually suggestive without being explicit.Send it to a person or to a stricter rule.
allowNeither of the above.Let it through.

sexual_level says how sexual the text is, independently of the score:

sexual_levelMeaning
noneNo sexual content.
contextualSex is mentioned without arousing intent: medical, educational, news.
suggestiveSexually suggestive without being explicit.
explicitExplicit sexual content.

category_scores.sexual is the probability of the explicit level. Suggestive text scores low there and shows up as sexual_level: "suggestive" and verdict: "review" instead, so read those two fields when suggestive content matters to you.

If you want stricter or looser lines than these, apply your own to category_scores.

policy_version identifies the policy that produced the scores. Scores from different policy versions are not comparable, so keep the version next to any score you store.

Limits

  • Text only. An image_url input is refused with a 400.
  • One text per call. An array with more than one item is refused with a 400 — nothing is partially judged. Send one request per text.
  • At most 8,000 characters. Longer text is refused with a 400 rather than cut short; split it and send each part. Length is counted in UTF-16 code units, so most emoji count as two.
  • Synchronous. There is no streaming and no task to poll. Server-side processing takes about 0.2 seconds.

Errors

Errors use the standard BeatAPI error envelope: code, message, retryable, request_id, and retry_after_seconds on a 429. See Errors for the full table and the retry policy. On this endpoint the error object also carries OpenAI’s type and param (param is always null), so the OpenAI SDKs raise their usual exceptions.

{
"error": {
"code": "bad_request",
"message": "input is required: the text to moderate",
"retryable": false,
"request_id": "…",
"type": "invalid_request_error",
"param": null
}
}
HTTP statusCodetypePython SDK raisesMeaning
400bad_requestinvalid_request_errorBadRequestErrorThe message says which rule was broken: empty input, several inputs, an image, over 8,000 characters, an unsupported model name, or invalid JSON.
401missing_api_key / invalid_api_keyinvalid_request_errorAuthenticationErrorNo key was sent, or the key is not valid.
402insufficient_creditsinsufficient_quotaAPIStatusErrorInsufficient balance. OpenAI itself answers an empty balance with a 429; here it is a 402, which SDKs do not retry automatically.
403forbiddeninvalid_request_errorPermissionDeniedErrorThe key or account may not use this model. An account that has never topped up cannot use it.
429rate_limit_exceededrate_limit_errorRateLimitErrorThe request limit was reached. Wait Retry-After seconds; the same number is in retry_after_seconds.
503processing_unavailableserver_errorInternalServerErrorThe moderation service could not answer. Retry with backoff.

The exceptions in the table are the ones the official Python SDK (openai 2.48.0) raised on 2026-10-12, except the 503 row, which follows the SDK’s status mapping and was not triggered.

A failed call judged nothing — never treat an error as “allowed”. A request refused with a 400 is not billed.

Billing

Moderation is billed on input tokens only, at $0.042 per 1M input tokens. beat-moderation-1 is listed on the pricing page.

The moderation policy travels with every call and counts as input, about 1,150 tokens, so even a short text has a floor:

TextInput tokensCost
One lineabout 1,200≈ $0.00005
8,000 Chinese charactersabout 9,200≈ $0.0004

usage.input_tokens in every response tells you exactly what the call counted, so you can reconcile spend per call.

Privacy

The text you submit is not written to request logs. Only its length, the scores and the policy version are kept.

What it does not do

  • It is a classifier, not a guarantee. Plain violations are caught reliably. Coded slang, homophones and disguised spam — especially in Chinese — are caught less reliably. Test it on your own content before relying on it.
  • sexual/minors does not replace hash-matching for known abuse material, or any legal reporting duty you have.
  • It does not check images, audio or video.
  • Scores are not exactly repeatable. In our tests they moved by up to 0.04 between identical calls, so a text that scores close to a line can land on either side of it. Treat a score near a line as borderline.

Agents

Agents reach the same check through the Capabilities API, where it is in the catalogue as model:beat-moderation-1. The Run input is {"input": "…"} and the result is the same body as above. Errors on that path use the standard envelope without type and param.