Text moderation
Screen text for policy violations
Text moderation evaluates text against your configured moderation policies, including any custom categories you have configured. Text judged prohibited is rejected, and the reason is returned on the completed job.
Input#
Submit a multipart/form-data request with:
| Field | Type | Required | Description |
|---|---|---|---|
text |
string | Yes | The text to moderate. |
webhook_url |
string | No | URL to receive the result on completion. |
curl -X POST https://api.omnifence.ai/api/v1/moderate/text \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "text=A sunset over the ocean"
How it's checked#
The text is checked against your policy configuration and every enabled
custom category on your account. If it is judged
prohibited, the job is rejected with is_prohibited: true and a short reason. When a custom
category caused the rejection, the reason names it by slug.
Output#
{
"is_prohibited": true,
"reason": "The text requests sexual content involving a minor.",
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "text",
"status": "completed",
"created_at": "2026-05-19T12:00:00.000Z",
"completed_at": "2026-05-19T12:00:01.500Z"
}
When is_prohibited is true, reason explains why the text was prohibited. Text that passes
returns is_prohibited: false with no reason. See decision logic for how
the prohibition decision is determined.
A completed job can also carry the optional scores and custom_scores fields:
{
"is_prohibited": false,
"scores": { "violence": 0.04, "self_harm": 0.01 },
"custom_scores": { "coffee": 0.02 },
"job_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"type": "text",
"status": "completed",
"created_at": "2026-11-09T12:00:00.000Z",
"completed_at": "2026-11-09T12:00:01.200Z"
}
They are informational risk scores and never change the decision. A text job rejected by the blocked-keyword list, or very long text, has no scores.
Batch text moderation#
Send up to 99 texts in one request with POST /api/v1/moderate/text/batch. Use a batch when one
action in your app produces several texts that each need their own decision, for example the fields
of a profile form.
- Each item is checked on its own, exactly as a single
/moderate/textsubmit checks it. The model never sees the other items of the batch. - Each item gets its own decision under the
keyyou give it, so you can show which field was rejected. - One batch request counts as one request against your rate limit. Each item is billed as one text moderation job.
- Each item is an ordinary text job with its own
job_id.GET /api/v1/job/{job_id}reads it, and the response names itsbatch_idandbatch_key.
Input#
Send a JSON body:
| Field | Type | Required | Description |
|---|---|---|---|
items |
array | Yes | 1 to 99 items. |
items[].key |
string | Yes | Your name for the item, unique in the batch. 1 to 64 letters, digits, _, ., : or -. |
items[].text |
string | Yes | The text to moderate. At most 20,000 characters. |
webhook_url |
string | No | URL that receives one webhook when every item is settled. Falls back to your registered webhook. |
curl -X POST https://api.omnifence.ai/api/v1/moderate/text/batch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "key": "name", "text": "Captain Mira" },
{ "key": "backstory", "text": "A pilot who left the fleet." }
],
"webhook_url": "https://your-app.com/webhooks/omnifence"
}'
const response = await fetch('https://api.omnifence.ai/api/v1/moderate/text/batch', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OMNIFENCE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{ key: 'name', text: 'Captain Mira' },
{ key: 'backstory', text: 'A pilot who left the fleet.' },
],
}),
});
const { batch_id, items } = await response.json();
The API accepts the batch with 202 and returns the batch ID and the job ID of each item:
{
"batch_id": "96cf89a8-4baa-44a4-a953-010f4486882d",
"status": "processing",
"items": [
{ "key": "name", "job_id": "b4c38526-b18b-431c-ab54-9e3b82bf26f0" },
{ "key": "backstory", "job_id": "6c3f2e78-a17a-44a9-b259-d3344eee8bf3" }
]
}
Validation#
The batch is accepted whole or refused whole. One invalid item refuses the request with 400, no
job is created and nothing is charged. The message names the item.
| Code | Cause |
|---|---|
BATCH_EMPTY |
items holds no item. |
BATCH_TOO_LARGE |
items holds more than 99 items. |
DUPLICATE_ITEM_KEY |
Two items have the same key. |
TEXT_TOO_LONG |
An item's text is longer than 20,000 characters. |
INVALID_REQUEST |
The body is not JSON, items is missing, or a key or text is invalid or empty. |
A credit balance at or below zero refuses the whole batch with 402 PAYMENT_REQUIRED, and no item
is accepted. With fixed pricing, the balance must also cover the price of every item. With
multiplier pricing, each item is charged when it completes, as a single job is.
Output#
When every item is completed or failed, the API sends one webhook with every item's
decision. See batch webhook. Items send no webhook of their own.
You can also read the batch with
GET /api/v1/moderate/text/batch/{batch_id}:
{
"type": "text_batch",
"batch_id": "96cf89a8-4baa-44a4-a953-010f4486882d",
"status": "completed",
"created_at": "2026-10-06T14:02:01.249Z",
"completed_at": "2026-10-06T14:02:03.494Z",
"items": [
{
"key": "backstory",
"job_id": "6c3f2e78-a17a-44a9-b259-d3344eee8bf3",
"status": "completed",
"is_prohibited": true,
"reason": "The text contained a blacklisted keyword."
},
{
"key": "name",
"job_id": "b4c38526-b18b-431c-ab54-9e3b82bf26f0",
"status": "completed",
"is_prohibited": false,
"scores": { "violence": 0.01 }
}
]
}
Each item carries is_prohibited, plus reason on a rejection and error_code on a failure. A
completed item can also carry the optional scores and custom_scores
risk scores, as a single text job does. A
failed item has status: "failed" and is_prohibited: null, it is refunded, and it does not change
the decision of any other item. Resubmit the failed text to get its decision.