Moderate text
Send a comment, a review, a message or one field of a form as text. The verdict comes back in the same response, with a score for every policy.
Text is the default content type: a comment, a review, a chat message, a bio, or one field of a larger form. It is screened while the request waits, so the response is the finished moderation and your code can act on it at once.
Send the text as input. type is text when you leave it out.
curl https://api.useinfrared.com/v1/moderations \
-H "Authorization: Bearer $INFRARED_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "text",
"input": "Is the oak table still available? I can collect it on Saturday."
}'const response = await fetch("https://api.useinfrared.com/v1/moderations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INFRARED_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "text",
input: "Is the oak table still available? I can collect it on Saturday.",
}),
});
const moderation = await response.json();import os
import requests
response = requests.post(
"https://api.useinfrared.com/v1/moderations",
headers={"Authorization": f"Bearer {os.environ['INFRARED_KEY']}"},
json={
"type": "text",
"input": "Is the oak table still available? I can collect it on Saturday.",
},
)
moderation = response.json(){
"object": "moderation",
"id": "mod_0X7lqCg6kqvuRBCfKEPXxa",
"template": { "slug": "basic", "version": 1 },
"type": "text",
"status": "completed",
"flagged": false,
"recommendation": "allow",
"severity": "none",
"policies": {
"toxicity": 0,
"hate_speech": 0,
"profanity": 0,
"violence": 0,
"self_harm": 0,
"sexual": 0,
"illicit": 0,
"spam": 0
},
"triggered": [],
"reasons": ["triage:allow"],
"language": "en",
"usage": { "checks": 0, "cached": false }, // sent with a test key; a live key uses 1
"latency_ms": 1267,
"test": true
}The moderation object describes every field of the response.
Which template runs
A request that names a template runs the published version of that template. A request that names none runs the project's default template, which you choose in Settings, Project under Default template. A project with no default runs the built-in basic check: eight policies, no rules of your own.
Say which field the text is
When the text is one field of a template, name the field. The sample sends the description of a listing to a template named listing, with your id for the listing and for the person who wrote it.
curl https://api.useinfrared.com/v1/moderations \
-H "Authorization: Bearer $INFRARED_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "listing",
"type": "text",
"field": "description",
"input": "Solid oak, minor marks on one leg. Order direct at https://shop.example.com/oak or call +44 7700 900123",
"content_id": "listing_18342",
"author_id": "user_42"
}'const response = await fetch("https://api.useinfrared.com/v1/moderations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INFRARED_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
template: "listing",
type: "text",
field: "description",
input: "Solid oak, minor marks on one leg. Order direct at https://shop.example.com/oak or call +44 7700 900123",
content_id: "listing_18342",
author_id: "user_42",
}),
});
const moderation = await response.json();import os
import requests
response = requests.post(
"https://api.useinfrared.com/v1/moderations",
headers={"Authorization": f"Bearer {os.environ['INFRARED_KEY']}"},
json={
"template": "listing",
"type": "text",
"field": "description",
"input": "Solid oak, minor marks on one leg. Order direct at https://shop.example.com/oak or call +44 7700 900123",
"content_id": "listing_18342",
"author_id": "user_42",
},
)
moderation = response.json(){
"object": "moderation",
"id": "mod_7ZgXCne4LiwQrsodlx4R8U",
"template": { "slug": "listing", "version": 1 },
"type": "text",
"field": "description",
"status": "completed",
"flagged": true,
"recommendation": "review",
"severity": "critical",
"triggered": ["personal_data", "guideline/no-off-site-deals", "instructions"],
"rule": { "id": "r6", "name": "Links from new accounts", "action": "review" },
"reasons": [
"trigger:personal_data:flag",
"trigger:guideline/no-off-site-deals:flag",
"trigger:instructions:flag",
"rule:r6"
],
"details": {
"personal_data": {
"labels": ["phone"],
"matches": [{ "label": "phone", "value": "+44 7700 900123", "start": 88, "end": 103 }],
"masked_output": "Solid oak, minor marks on one leg. Order direct at https://shop.example.com/oak or call +44***23",
"field": "description"
},
"guideline/no-off-site-deals": {
"labels": ["confidence:100"],
"matches": [{ "label": "evidence", "value": "Order direct at https://shop.example.com/oak or call +44 7700 900123", "field": "description" }],
"explanation": "The listing explicitly instructs buyers to complete the transaction on an external website or via phone, which is a clear violation of the no off-site deals policy.",
"field": "description"
}
}
}Three things flagged this description: the phone number, one of the project's guidelines and the template's AI instructions. A content rule then sent it to review. How a decision is made walks through this same result.
Naming the field changes three things:
- The field's own validation applies: its length limits, its pattern, its allowed values.
- The models are told the field's name and what it holds, so "Price" and "Description" are judged as what they are.
- The field's triggers apply beside the template's, and every entry in
detailssays which field it is about.
Without field, the text could be any field of the template, so every field-level trigger of the template applies to it. A text may name any field that is not an image field.
Limits on the text
inputholds up to 20,000 characters, counted as Unicode code points, and must not be empty.- The whole request body may be 2 MB.
A text that is empty or too long is not an HTTP error. The API answers 200 with status: "invalid", uses no check and says why in validation_errors. Statuses and errors shows the shape.
What happens to the text first
Before any policy reads it, the text is normalised:
- Invisible characters are removed: zero-width spaces and joiners, soft hyphens, direction marks.
- Unicode compatibility forms are unified, so full-width letters and circled digits become plain ones.
- Lookalike letters are folded to the alphabet of their word.
Gοοglewritten with Greek omicrons is read asGoogle. - Runs of spaces collapse and the ends are trimmed.
Policies score the normalised text. The positions in the evidence follow it too: start, end and masked_output in details index the normalised text, which differs from your input whenever a step above changed something.
What was removed and folded is itself a signal. It feeds the lookalike_characters policy when a template enables it. Zero-width joiners count as invisible characters, so an emoji built from several joined emoji raises that policy's score.
Language
Every text moderation carries language, an ISO 639-1 code such as en or pt. A local detector answers first for texts of 12 letters or more, and the language the text classifier reports replaces it whenever a model-backed policy runs. For shorter texts a template can supply a language hint.
When no model-backed policy runs on a text, the local answer stands, and it can be wrong on a short sentence. language is null when nothing could tell.
Policies that need no model
Six policies get their answer, or part of it, from patterns, lists and hashes, with no model call. Each runs only when the template enables it. On the basic check that is the lexicon behind profanity.
| Policy | What it finds | Evidence in details |
|---|---|---|
personal_data | Emails, phone numbers, card numbers, IBANs and national ID numbers. Street addresses when the template turns them on. | Each match with its position, and masked_output |
links_contact | URLs, messenger and short links, @handles | Each match with its position |
lookalike_characters | Invisible characters and words that mix alphabets through a lookalike letter | The words as they were written |
profanity | Swear words from a built-in lexicon, including masked, spaced-out and stretched spellings | The word it matched |
wordlist/<slug> | The terms of one of your wordlists | The matched word and its canonical term |
duplicate | The same text seen earlier in the project | The content_id of the earlier submission |
The model-backed policies are scored in one call. If that call fails, these six still answer and the moderation ends with status: "partial". Policies describes each policy and its options.
Cost
A text uses one check, whatever the number of policies.
Identical text on the same template version within six hours is served from the result cache. usage.cached is then true, and each policy in details carries cached: true. A cached result costs a quarter of the checks and never less than one, so a single cached text still uses one check.
The cache is keyed by the text, the template version and your project context. A term added to a wordlist since, or a domain added to the allowed list, does not reach a cached text until the entry expires. The template test never reads the cache, so it is the place to try such a change. Cache results in Settings, Project turns the cache off.
Next
- Moderate imagesSend an image by URL. Infrared fetches it, screens the picture and reads the text inside it. The result arrives by webhook or by polling.
- Moderate a whole formSend every field of a listing, a profile or a post in one request. Each content field gets its own result and the parent carries the decision.
- The moderation objectThe verdict, the scores, the evidence in details and your own context. Every field of a moderation, and when each one is present.
- TemplatesA template is the configuration for one kind of content. It holds the fields, the policies and the rules, and requests name it by its slug.
Something missing or unclear on this page? Write to support@useinfrared.com.