Moderations

Checks text and images against content categories with a moderation model on your own worker. OpenAI-compatible.

POST /v1/moderations

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:

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

Body application/json
model string Required
The moderation model to check with, for example llama-guard3:1b. It must be installed on one of your workers.
input string | string[] | object[] Required
What to check, in one of three forms:
  • 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: true if the model judged the input unsafe. A flagged result always has at least one category set to true.
  • categories: each category the model checks, true if the input falls into it.
  • category_scores: the same categories as numbers: 1.0 (flagged), 0.0 (not flagged), or 0.5 when a qwen3guard model flags it as borderline (content that could be harmful depending on context). A borderline result is still flagged; 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 on text, image or 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/.

CategoryCovers
harassment, harassment/threateningHarassing or defamatory content, and harassment that includes threats.
hate, hate/threateningHateful content about a protected group, and hate that includes threats.
illicit, illicit/violentHelp with crimes, and with violent crimes or weapons.
self-harm, self-harm/intent, self-harm/instructionsSuicide and self-harm content, stated intent, and instructions.
sexual, sexual/minorsSexual content, and any sexual content involving minors.
violence, violence/graphicViolent content, and graphic depictions of it.
pendra/cyberMalware, intrusion, and abuse of code-running tools.
pendra/disinformationElection misinformation and other deliberate deception.
pendra/fraudScams, phishing and forged documents.
pendra/intellectual_propertyCopyright and trademark abuse, counterfeits.
pendra/nudityNudity, including non-sexual. A model that can't tell nudity from sexual content reports both.
pendra/privacyExposing someone's personal or sensitive information.
pendra/specialized_adviceUnqualified medical, legal or financial advice.
pendra/unspecifiedFlagged, but the model didn't name a category.

Which categories each model reports:

ModelInputCategories 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."
}
StatuscodeMeaningRetry?
400model_not_moderationThe model isn't a moderation model.No. Use a model with the moderation capability.
400image_input_not_supportedYou sent an image to a model that checks text only.No. Use a model that checks images.
400encrypted_input_not_supportedThe request was end-to-end encrypted. This endpoint doesn't accept private inference requests.No. See below.
404model_not_foundThe model id isn't in the catalogue.No. Check the id.
409moderation_unavailableNo 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.
429moderation_capacityEvery worker with the model is busy.Yes, after Retry-After seconds.
502moderation_invalid_verdictThe 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.
503moderation_unavailableThe 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:1b typically 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.