Moderation runs on every write request
Text fields on write requests are moderated before generation starts. A valid JSON payload can still 400; here is how to surface that.
A generate POST can be schema-valid, authenticated, in budget, and still return 400 with error: "Content policy violation" before any model is contacted. That is the moderation middleware, and it runs on every POST, PUT, and PATCH that carries the text fields it knows about. If you treat every 400 as "fix the JSON," you will retry the same prompt, spend nothing, and show your user a parser error that is not what happened.
The check is a gate on your input, not a score of the output. Generation has not started. Credits have not been taken for that job. The correct product behaviour is to show a specific, human message and refuse to resubmit the same string.
What actually gets read
The middleware concatenates these body fields, in this order, if they are non-empty strings:
message, prompt, text, caption_text, transcript, style_prompt
Plus every messages[].content where role is "user" (chat-shaped bodies).
If that concatenation is empty, the request proceeds. GET, DELETE, and OPTIONS skip the filter entirely, which is why catalog reads and GET /api/v1/status/:requestId are not moderated. A generate call with a prompt is a write, and the prompt is the thing being classified.
Only the first 2,000 characters of the concatenated string are classified. A long script with the problem in paragraph fourteen can pass this gate and still fail later, for other reasons. Do not treat "moderation said 200" as "the whole document is fine." Put the load-bearing text at the front, or moderate it yourself before you send.
Images, audio, and video bytes are not inspected here. A clean prompt with a disallowed still attached is a different class of failure, further downstream, if it fails at all. This gate is text.
Two layers, one 400
A small set of unambiguous high-risk phrases is blocked with a regular-expression pre-filter, without calling a classifier. That layer exists so the obvious cases cannot depend on a model being in a generous mood.
Everything else goes to a JSON classifier with a fixed category list. The categories that come back on a block are:
category |
What it means on this platform |
|---|---|
CSAM |
Sexual content involving minors. |
Hate Speech |
Explicit calls for violence against, or dehumanisation of, a protected group. |
Dangerous |
Instructions for weapons, explosives, or controlled substances, including "how do I…" phrasing. |
Harassment |
Targeted threats or doxxing of a specific person. |
Non-consensual sexual content |
Deepfakes or explicit content of real people without consent. |
BLOCKED_BY_PROVIDER |
The upstream classifier refused to return a body. Treat as a block. |
The policy is deliberately narrow on the other side. Creative prompts, advertising, mild profanity, horror and noir described abstractly, satire, historical violence, song lyrics, and anything ambiguous are supposed to pass. "When in doubt, mark as safe" is the written rule. A glamour photoshoot prompt should not 400. A bomb-making prompt should.
The response on a block is always this shape:
{
"error": "Content policy violation",
"category": "Dangerous",
"message": "Your request contains content that violates our usage policy. Please modify your input and try again."
}
You get category. You do not get the classifier's internal reasoning. Do not log the prompt into a public error page. Do not retry. Map category to copy your own user is allowed to see.
If the classifier itself is down, the middleware fails open after retries: the request proceeds, and only the regex pre-filter has had a chance to block. That is a reliability choice, not a promise that a later provider will accept the same text. Your own product policy can still be stricter. A 400 with Content policy violation is a hard block; the absence of that 400 is not a legal opinion.
This is a safety checker on the way in, not a substitute for brand-safety on the way out. You still decide what your product is willing to show.
How to surface it in your product
A structurally valid payload that 400s here looks, to a naive client, like every other 400 (Model is required, clips[0].url is required, invalidModels). Branch on the string.
if status == 400 and body.error == "Content policy violation":
show category_copy(body.category)
do not retry
do not send the user to "fix your JSON"
Suggested copy, which you should rewrite in your voice:
- CSAM / Non-consensual sexual content. "This request was blocked. We cannot generate that." No "try rephrasing" hint.
- Dangerous. "This request asks for instructions we do not generate. Rephrase to a scene or a product, not a how-to."
- Hate Speech / Harassment. "This request was blocked as a threat or as hate speech. Remove the targeted language."
- BLOCKED_BY_PROVIDER. "This request was blocked by the safety filter. Try a different description of the scene."
- Unknown category. Use the generic
messagefrom the body.
Three implementation details that matter more than the copy:
- Do not round-trip the same prompt. Retrying a policy 400 will 400 again and will not produce a
request_id. Your job table should recordstatus = rejected_policywith the category, notpendingwith another attempt. - Tell the user which field you sent. If you concatenated a caption and a prompt, say so. They cannot edit what they cannot see. The prompt they typed is usually the whole story;
style_promptandcaption_textare the ones people forget they filled in. - Keep your own denylist if you have one. This gate is Versely's floor. A toy brand that never wants horror still has to filter horror; the middleware will let "noir alley, rain, 35mm" through.
The developer error ladder still applies around this: 401 / 403 / 402 / 429 are not policy. Do not fold them into the same toast. A 402 is credits; there is no free allowance to "just try it." Pricing is that conversation. A policy 400 is content.
What this means for agents and batch jobs
An agent that retries 400s will loop. The generate skill's error ladder is written for 401 / 402 / 403 / 429 / 500. Add Content policy violation next to 402 as a stop: show the category, ask the human for a new prompt, do not call generate again. The agent will do that in a conversation; your worker has to do it in code.
A batch of 200 prompts should pre-scan for empty prompts (those skip the filter and then 400 on Prompt is required) and should treat a policy 400 as a per-row terminal failure, not a reason to halt the batch. The next row is a different string. Log the category on the manifest. Do not log the full prompt to a shared channel.
Because only named text fields are read, a body that puts the user text in input or ssml (audio generate) or scenes[].description (story) may not hit this middleware even though a provider-side filter might. Do not use that as a bypass. Put the user-visible text in prompt / text / message as the endpoint documents, and assume a later gate can still refuse.
GET catalog calls, text-to-image page loads, and status polls are not this filter. You will not 400 on a read.
FAQ
Why did a schema-valid generate POST 400 with no request_id?
Moderation runs first. No job is created, no status URL exists, no credits are taken for that call. Read error and category. Change the text. Then POST once.
Can I turn the filter off with a query flag?
No. It is global on write methods. Empty text skips it because there is nothing to classify, not because you opted out.
Does a pass mean the output is allowed on YouTube / Instagram / TikTok?
No. This is Versely's input policy. Platform originality, disclosure, and inauthentic-content rules are a different checklist, applied to the file you later upload. Passing moderation here does not label the asset and does not indemnify the upload.
Will you tell me which sentence triggered it?
Not in the JSON. You get a category and a generic message. If you need to debug, look at the fields listed above, starting with prompt. Do not paste the blocked prompt into a second product to "see if it works there"; fix it or drop the row.