Content Safety API
The content safety endpoint scores a text on seven moderation categories — toxicity, profanity, harassment, hate speech, self-harm, sexual content, and violence — each with a probability from 0 (absent) to 1 (present). Use it to moderate user-generated content, screen LLM outputs before they reach users, or route messages for human review.
For simple token-level profanity flagging, see the profanity endpoint. For determining whether text is AI-generated, see the AI detector.
Sample Code
- cURL
- JavaScript
- Python
curl -X POST https://api.sapling.ai/api/v1/safety \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text":"You are a worthless idiot and everyone hates you."}'
import axios from 'axios';
async function run(text) {
try {
const response = await axios.post(
'https://api.sapling.ai/api/v1/safety',
{
key: '<api-key>',
text,
},
);
const {status, data} = response;
console.log({status});
console.log(JSON.stringify(data, null, 4));
} catch (err) {
const msg = err.response?.data?.msg || err.message;
console.log({err: msg});
}
}
run('You are a worthless idiot and everyone hates you.');
import requests
from pprint import pprint
response = requests.post(
"https://api.sapling.ai/api/v1/safety",
json={
"key": "<api-key>",
"text": "You are a worthless idiot and everyone hates you."
}
)
if 200 <= response.status_code < 300:
pprint(response.json())
else:
print('Error: ', response.status_code, response.text)
Sample Response
{
"flagged": true,
"flagged_categories": ["toxicity", "harassment"],
"scores": {
"toxicity": 0.92,
"profanity": 0.15,
"harassment": 0.88,
"hate_speech": 0.05,
"self_harm": 0.0,
"sexual": 0.0,
"violence": 0.02
},
"threshold": 0.5
}
Batch Requests
To score many short texts — a queue of chat messages, a page of reviews — in one request, send
texts (a list of 1–10 strings) instead of text. The same threshold and spans options apply
to every item, and the combined length of all items may be up to 20,000 characters (the same cap as
a single text, measured on the submitted text — before HTML stripping). Exactly one of text
and texts must be provided.
curl -X POST https://api.sapling.ai/api/v1/safety \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "texts": ["You are a worthless idiot.", "What a lovely afternoon!"]}'
The batch response is {"results": [...]} with one entry per input, in input order; each entry has
exactly the single-response shape above:
{
"results": [
{
"flagged": true,
"flagged_categories": ["toxicity", "harassment"],
"scores": {
"toxicity": 0.92,
"profanity": 0.15,
"harassment": 0.88,
"hate_speech": 0.05,
"self_harm": 0.0,
"sexual": 0.0,
"violence": 0.02
},
"threshold": 0.5
},
{
"flagged": false,
"flagged_categories": [],
"scores": {
"toxicity": 0.01,
"profanity": 0.0,
"harassment": 0.0,
"hate_speech": 0.0,
"self_harm": 0.0,
"sexual": 0.0,
"violence": 0.0
},
"threshold": 0.5
}
]
}
With spans: true, each entry's span offsets index that entry's own submitted text. Items are
billed individually (a batch of N costs the same as N single requests), and each item is cached the
same way as a single request — batches and single calls share the cache. If any item fails to score,
the whole request returns a 400 and nothing is billed; the items that did succeed are already
cached, so a retry only re-runs the failures.
Request Parameters
POST to https://api.sapling.ai/api/v1/safety
key: String
32-character API key. Can also be supplied via the Authorization header as a bearer token; if both are provided, the key parameter takes precedence.
text: String
Text to score, up to 20,000 characters (HTML tags are stripped before scoring). Provide exactly one of text and texts.
texts: List[String]
Batch form (see Batch Requests): 1–10 texts to score in one request, each treated
exactly like text above, with a combined length of up to 20,000 characters. An item that is empty
after HTML stripping returns a 400 naming its index. The response becomes {"results": [...]},
one entry per item in input order.
threshold: Float, optional, defaults to 0.5
Score at or above which a category is included in flagged_categories.
Between 0 and 1 inclusive. Raise it to only flag high-confidence violations;
lower it for stricter moderation. The threshold only affects the flags —
the raw scores are always returned.
spans: Boolean, optional, defaults to false
When true, the response also includes spans: the specific offending passages,
each with its text, character offsets, and per-passage category scores.
Use it to highlight, quote, or redact just the violating passage instead of
rejecting the whole text.
Response Parameters
flagged: Boolean
True if any category scored at or above the threshold.
flagged_categories: List[String]
Categories that scored at or above the threshold.
scores: Object
A probability from 0 to 1 for each category:
| Category | Description |
|---|---|
toxicity | Rude, disrespectful, or unreasonable language overall |
profanity | Swear words or vulgar language |
harassment | Attacks, bullying, or intimidation directed at a person or group |
hate_speech | Attacks on protected characteristics (race, religion, gender, etc.) |
self_harm | Encouragement or expression of self-harm or suicide |
sexual | Sexually explicit content |
violence | Threats, incitement, or graphic descriptions of violence |
threshold: Float
The threshold that was applied.
spans: List[Object] (only with spans: true)
The offending passages, most severe first. Each span has:
text: the passage, quoted from the submitted text.start/end: character offsets of the passage in the submitted text (text[start:end]is the passage). Offsets count Unicode code points (Pythonstrindexing) — the same unit as the quality API'ssentences— not UTF-16 code units or bytes. Both arenullwhen the passage cannot be located in the submitted text — for example when it sits inside stripped HTML markup.scores: the category probabilities reported for this passage alone. Only categories the passage expresses are included; passages scoring below 0.25 on every category are not reported.flagged_categories: the passage's categories at or above the requestthreshold. Spans with no category at or above the threshold are omitted entirely.
With spans: true the request above returns:
{
"flagged": true,
"flagged_categories": ["toxicity", "harassment"],
"scores": {
"toxicity": 0.92,
"profanity": 0.15,
"harassment": 0.88,
"hate_speech": 0.05,
"self_harm": 0.0,
"sexual": 0.0,
"violence": 0.02
},
"threshold": 0.5,
"spans": [
{
"text": "You are a worthless idiot and everyone hates you.",
"start": 0,
"end": 49,
"scores": {"toxicity": 0.92, "harassment": 0.88},
"flagged_categories": ["toxicity", "harassment"]
}
]
}
Tips
- Scores are calibrated per category: a text can flag multiple categories at once
(for example, a threat is usually both
toxicityandviolence). - Quoted, negated, or educational mentions of a topic score lower than direct expressions, but consider a lower threshold if your application must be conservative.
- Results for identical text are cached briefly on Sapling's side, so re-checking the same content does not change scores.
- Spans can overlap (a sentence and a longer passage containing it may each be
reported); when highlighting, use the
start/endoffsets to merge or de-duplicate overlapping regions. - Because offsets are Unicode code-point indices, slice with
Array.from(text)in JavaScript if the input may contain characters outside the Basic Multilingual Plane — its UTF-16 string indices would otherwise misalign.