Moderations
Checks text and images against content categories with a moderation model on your own worker. OpenAI-compatible.
Send text or images and get back, for each input, whether it was flagged and which content categories it fell into. Use it to screen what your users send before it reaches a chat model, or to check what a model wrote before you show it.
The endpoint is also available at /api/v1/moderations. The
request and response follow the OpenAI moderations shape, so the OpenAI
SDKs' moderations.create works unchanged once you point the
client's base URL at https://api.pendra.ai/v1. Authenticate
with an API key that has the inference:write
permission, the same
one chat and embeddings use.
Before you start: install a moderation model
The check runs on a moderation model (a small model trained to label
content as safe or unsafe) that you install from the catalogue on one of
your own workers, like any other model. The content you send is checked on
one of your organisation's workers. Moderation models carry the moderation
capability in the catalogue. Moderation models in the catalogue include:
| Model | Checks | What you get back |
|---|---|---|
llama-guard3:1b, llama-guard3:8b |
Text | A verdict and the categories it falls into. The 8B size catches more and wrongly flags less; the 1B size is much faster and smaller. |
qwen3guard:0.6b, qwen3guard:4b, qwen3guard:8b |
Text, in many languages | A verdict, which can be borderline, and the categories it falls into. Larger sizes catch more; the 0.6B size is the fastest. |
singguard:4b, singguard:0.8b |
Text and images | A verdict and the categories it falls into. The 4B size catches more; the 0.8B size is faster on small GPUs. |
A worker runs one generation at a time by default, so a moderation check can wait behind a long chat reply on the same worker. If you check content on every request, install the moderation model on a worker that isn't busy with long generations.
model
string
Required
llama-guard3:1b. It must be installed on one of your workers.
input
string | string[] | object[]
Required
- A string: one result.
- An array of up to 32 strings: one result per string, in the same order.
- An array of up to 32 parts, each
{"type": "text", "text": "..."}or{"type": "image_url", "image_url": {"url": "..."}}: the parts are checked together and give one result, so an image and its caption are judged as a whole.
Checking images
Send images as parts, to a moderation model that checks images (such as
singguard:4b). Sending an image to a text-only model returns
400 image_input_not_supported.
{
"model": "singguard:4b",
"input": [
{ "type": "text", "text": "Profile photo for a new account" },
{ "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4AAQ..." } }
]
}
A base64 data: URL always works. An https://
image URL works only if the owner of the worker has turned on
Allow remote image URLs, the same rule as
images in chat.
Response
A results array with one entry per input string (or a single
entry for an array of parts). Each result has:
flagged:trueif the model judged the input unsafe. A flagged result always has at least one category set totrue.categories: each category the model checks,trueif the input falls into it.category_scores: the same categories as numbers:1.0(flagged),0.0(not flagged), or0.5when aqwen3guardmodel flags it as borderline (content that could be harmful depending on context). A borderline result is stillflagged; read the score if you want to treat it differently. Moderation models give labels rather than probabilities, so there is no threshold to tune.category_applied_input_types: for each category, whether it was judged ontext,imageor both.
Only the categories the chosen model checks are present.
A category missing from the result means that model doesn't check it, not
that the content is clean. The response also carries the
pendra field naming the
worker that ran the check.
Categories
Pendra uses OpenAI's category names where one fits, and adds its own under
pendra/.
| Category | Covers |
|---|---|
harassment, harassment/threatening | Harassing or defamatory content, and harassment that includes threats. |
hate, hate/threatening | Hateful content about a protected group, and hate that includes threats. |
illicit, illicit/violent | Help with crimes, and with violent crimes or weapons. |
self-harm, self-harm/intent, self-harm/instructions | Suicide and self-harm content, stated intent, and instructions. |
sexual, sexual/minors | Sexual content, and any sexual content involving minors. |
violence, violence/graphic | Violent content, and graphic depictions of it. |
pendra/cyber | Malware, intrusion, and abuse of code-running tools. |
pendra/disinformation | Election misinformation and other deliberate deception. |
pendra/fraud | Scams, phishing and forged documents. |
pendra/intellectual_property | Copyright and trademark abuse, counterfeits. |
pendra/nudity | Nudity, including non-sexual. A model that can't tell nudity from sexual content reports both. |
pendra/privacy | Exposing someone's personal or sensitive information. |
pendra/specialized_advice | Unqualified medical, legal or financial advice. |
pendra/unspecified | Flagged, but the model didn't name a category. |
Which categories each model reports:
| Model | Input | Categories reported |
|---|---|---|
llama-guard3:1b, llama-guard3:8b |
Text | harassment, hate, illicit, illicit/violent, self-harm, sexual, sexual/minors, violence, pendra/cyber, pendra/disinformation, pendra/fraud, pendra/intellectual_property, pendra/privacy, pendra/specialized_advice, pendra/unspecified. Only llama-guard3:8b checks for abuse of code-running tools, so pendra/cyber is reported but never set by llama-guard3:1b. |
qwen3guard:0.6b, qwen3guard:4b, qwen3guard:8b |
Text | harassment, hate, illicit, illicit/violent, self-harm, sexual, violence, pendra/cyber, pendra/disinformation, pendra/fraud, pendra/intellectual_property, pendra/nudity, pendra/privacy, pendra/unspecified. It has no separate category for sexual content involving minors (it reports it as sexual), so sexual/minors is not checked by this model. A detected jailbreak attempt is reported as pendra/unspecified. |
singguard:4b, singguard:0.8b |
Text, images | harassment, hate, illicit, self-harm, sexual, violence, pendra/disinformation, pendra/fraud, pendra/nudity, pendra/unspecified |
Try it in the Playground
Open the Playground and pick a moderation model, or go straight to
Moderation mode at
https://console.pendra.ai/playground?mode=moderation.
Paste text or attach an image to see the verdict for each category. An
image is only shown once it has passed the check.
Errors
The errors below carry error.message,
error.type and error.code, and the code is also
sent in the X-Pendra-Error-Code header, so you can branch on
it without reading the message:
{
"error": {
"message": "Moderation model 'llama-guard3:1b' checks text only. Send images to a moderation model that checks images, such as singguard:4b.",
"type": "invalid_request_error",
"code": "image_input_not_supported",
"param": null
},
"detail": "Moderation model 'llama-guard3:1b' checks text only. Send images to a moderation model that checks images, such as singguard:4b."
}
| Status | code | Meaning | Retry? |
|---|---|---|---|
400 | model_not_moderation | The model isn't a moderation model. | No. Use a model with the moderation capability. |
400 | image_input_not_supported | You sent an image to a model that checks text only. | No. Use a model that checks images. |
400 | encrypted_input_not_supported | The request was end-to-end encrypted. This endpoint doesn't accept private inference requests. | No. See below. |
404 | model_not_found | The model id isn't in the catalogue. | No. Check the id. |
409 | moderation_unavailable | No online worker has the model: none is connected, it isn't installed, it's switched off, or it doesn't fit. Also returned when the only workers with it run a worker version too old to run moderation models; update them. | No. Install the model, bring a worker online, or update the worker, then try again. |
429 | moderation_capacity | Every worker with the model is busy. | Yes, after Retry-After seconds. |
502 | moderation_invalid_verdict | The model replied with something that was neither safe nor unsafe, so there's no verdict to return. | Not usually: the same input tends to get the same reply. |
503 | moderation_unavailable | The worker running the check failed or timed out. | Yes, after Retry-After seconds. |
A malformed body (an empty input, more than 32 items, or strings and parts
mixed in one array) gets the usual 422,
and a key without inference:write gets a 403.
Errors on a very slow request
If a check waits long enough that Pendra has started keeping the
connection alive (about 80 seconds, for example behind a long reply on the
same worker), a failure after that point arrives as a 200
whose body is an error object with no results,
in the same shape as other endpoints:
code is the status the response would have had,
moderation_code the code from the table above, and
retry_after the back-off where there is one. Treat any
response without results as not checked.
To send a check to one particular worker, add the
X-Pendra-Worker-Id header, as on other inference endpoints.
End-to-end encryption
Content you send to this endpoint is not end-to-end encrypted. It's
encrypted in transit like every Pendra request, and checked on your own
worker. End-to-end encrypted requests get 400
encrypted_input_not_supported, and if your organisation has
Require E2E turned on, other requests get the usual
403 e2e_required.
So organisations that require end-to-end encryption can't use this
endpoint yet: it doesn't accept encrypted requests, and such an
organisation has chosen that its content is never sent unsealed.
To check encrypted chat requests and answers automatically, turn on content filtering for the API key instead. That works with end-to-end encryption: the worker that answers runs the check, and Pendra sees only the outcome.
Usage and privacy
Each call appears in your usage log
like any other request, with request_type
moderation, its outcome (pass,
flagged or error) and the names of any flagged
categories.
When you call the moderation endpoint, Pendra records the verdict labels (outcome and categories) in your usage logs, with the usual usage details for the check, never the content you sent.
Notes
- Moderation is content filtering against the categories above. A moderation model can miss harmful content or flag harmless content, so treat its verdict as one signal in your own process, not a guarantee.
- The endpoint only returns verdicts. It never blocks or changes anything itself: what to do with flagged content is your decision and your content policy.
- Moderation is off unless you use it. Content is checked only when you call this endpoint, with a model you installed; your other requests are not checked.
- A small text model such as
llama-guard3:1btypically answers in well under a second on a modern GPU. Each string in an array is a separate check, run one after another, so a request with 32 strings takes roughly 32 times as long as one.