Statuses and errors
The five statuses a moderation can have and every error the moderation endpoints return, with the HTTP status, the code and what to do next.
A request to moderate can go wrong in two different ways. The API can refuse it: the answer is a 4xx with an error body, and no moderation exists. Or the API can accept it and return a moderation whose status says the result is not a complete screening. Your code needs a branch for each.
One case sits where people do not expect it. A submission that breaks the template's validation is not refused: it answers 200 with a moderation whose status is invalid.
The five statuses
status | What it means | recommendation | Checks |
|---|---|---|---|
screening | Accepted and queued, or running. The request answered 202. | null | None yet |
completed | Every policy answered. | Computed | Used |
partial | At least one policy did not answer. The others stand. | Computed from the policies that answered | Used in full |
failed | The run itself did not finish. | null | None |
invalid | The submission failed validation. Nothing was screened. | reject | None |
Only completed and partial carry a recommendation that came from screening.
Invalid submissions
Validation runs before any model and costs nothing. This form breaks four rules of a template named listing: an unknown field, a title that is too short, a missing description and a negative price.
curl https://api.useinfrared.com/v1/moderations \
-H "Authorization: Bearer $INFRARED_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "listing",
"content_id": "listing_18343",
"fields": {
"title": "Oak",
"price": -5,
"colour": "brown"
}
}'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",
content_id: "listing_18343",
fields: {
title: "Oak",
price: -5,
colour: "brown",
},
}),
});
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",
"content_id": "listing_18343",
"fields": {
"title": "Oak",
"price": -5,
"colour": "brown",
},
},
)
moderation = response.json(){
"object": "moderation",
"id": "mod_2JVuJCJ0O0vr3KhzCfN6wY",
"template": { "slug": "listing", "version": 1 },
"type": "form",
"status": "invalid",
"flagged": true,
"recommendation": "reject",
"severity": "none",
"policies": {},
"triggered": [],
"rule": null,
"reasons": ["validation_failed"],
"details": {},
"validation_errors": [
{ "field": "colour", "code": "unknown_field", "message": "colour is not a field of this template" },
{ "field": "title", "code": "min_length", "message": "Title must be at least 5 characters" },
{ "field": "description", "code": "required", "message": "Description is required" },
{ "field": "price", "code": "min", "message": "Price must be at least 0" }
],
"usage": { "checks": 0, "cached": false },
"content_id": "listing_18343"
}Each entry names the field, a stable code and a message you can show. The message uses the field's display name from the template.
code | The value |
|---|---|
required | is missing or empty, and the field is required |
type | is not the kind the field takes: text, a number, true or false, an object |
min_length, max_length | is shorter or longer than the field allows, or longer than 20,000 characters |
numeric, pattern, enum | does not match the field's format or its allowed values |
min, max | is a number outside the field's range |
min_items, max_items | holds too few or too many files |
url | is not an http or https URL |
unknown_field | belongs to a field the template does not have |
type_mismatch | was sent as text to an image field, or as an image to a text field |
For a single object sent without field, the entry's field is input for a text and null for an image. When the field parameter itself is wrong, it is the word field.
An invalid moderation is still a moderation. It has an id, shows in the Live console and is reported by a moderation.completed event. It does not enter the review queue and does not count in the author's history.
Partial results
partial means one or more policies did not answer: a model call failed or timed out, or an image could not be fetched. The decision was made on the rest.
{
"object": "moderation",
"id": "mod_3awxoUbFeEr4cO37Rim1mh",
"type": "image",
"status": "partial",
"flagged": false,
"recommendation": "review",
"severity": "none",
"policies": { "toxicity": 0, "hate_speech": 0, "profanity": 0, "spam": 0 },
"reasons": ["triage:allow", "partial:violence,self_harm,sexual,illicit", "partial_review"],
"details": {
"violence": { "error": "HTTP 404 fetching media" },
"self_harm": { "error": "HTTP 404 fetching media" },
"sexual": { "error": "HTTP 404 fetching media" },
"illicit": { "error": "HTTP 404 fetching media" }
}
}Three places say which policies are missing: they are absent from policies, named in one partial: entry of reasons, and listed in details with an error. When a missing policy was able to trigger, an allow becomes review, so content does not pass on a screening that did not happen. A recommendation set by one of your content rules is kept as it is.
A partial result uses its checks in full.
Failed runs
failed means the run did not finish at all. error holds code: "pipeline_error" and a message, there is no recommendation, and no check is used. A moderation.failed event is sent. Treat the content as not screened and send it again.
Errors that refuse the request
Every error has the same body. code is stable and is what your code should branch on; request_id is also in the X-Request-Id response header.
{
"error": {
"code": "quota_exceeded",
"message": "The monthly checks quota is exhausted and no extra checks are available",
"details": [{ "field": "checks", "message": "1 needed, 0 available" }],
"request_id": "req_d04336b3bb314aa594a223d16009a477"
}
}| HTTP | code | When | What to do |
|---|---|---|---|
| 400 | invalid_json | The body is not valid JSON. | Fix the body. |
| 400 | not_supported_yet | type is audio or video, or a form holds a value in an audio or video field. | Leave those values out. |
| 401 | missing_api_key, invalid_api_key | No bearer key, or one that is unknown, revoked or expired. | Check the key. |
| 402 | quota_exceeded | A live key, and the monthly checks plus extra checks cannot cover this request. | Buy extra checks or wait for the reset. See Checks. |
| 403 | insufficient_scope | The key lacks the moderate scope, or read for a GET. | Use a key with the scope. |
| 403 | plan_limit | The request names a template on a plan without templates, or a batch is larger than the plan allows. | Send it without template, shrink the batch, or change plan. |
| 404 | template_not_found | No template has that slug, or it is archived. | Check the slug. |
| 404 | not_found | The moderation id is unknown or belongs to another project. | Check the id and the key. |
| 409 | template_not_published | The template has no published version yet. | Publish it. |
| 409 | idempotency_key_reused, idempotency_in_progress | The Idempotency-Key was used with another body, or its first request is still running. | See Idempotency. |
| 409 | input_not_stored, is_child | A re-moderation of content that was not stored, or of one field of a form. | See Dry run and re-moderation. |
| 413 | payload_too_large | The body is over 2 MB. | Send fewer or shorter items. |
| 422 | validation_error | The body or the query does not match the schema. | Read details. |
| 429 | rate_limited | The key passed its requests per minute. | Wait for Retry-After seconds. See Rate limits. |
| 500 | internal_error | Something failed on Infrared's side. | Retry with the same Idempotency-Key. |
Details worth knowing
422 is about the shape of the request, not the content. A body is either a single object or a form, and when it fits neither, details often says only { "field": "body", "message": "Invalid input" }. A missing input, a misspelt mode or type, a nested value in metadata and fields sent together with input all read that way. An unknown key is named: Unrecognized key: "priority".
Validation comes before the quota. A submission that is invalid answers 200 with status: "invalid" even when no checks are left, because it would use none.
A template is never swapped for another. On a plan without templates, a request that names one answers 403 whether or not the template exists. The basic check answers to the name basic on every plan.
The default template has to be published. When the project's default template has no published version, requests that name no template answer 409 template_not_published too.
An error is replayed under the same key. A 4xx answer is stored with its Idempotency-Key for 24 hours. After you fix the cause, send the request with a new key.
Next
- ErrorsEvery error has one JSON shape. Branch on its code, show its details and quote its request id. The table lists each code the API returns and when.
- How a decision is madeTriggers set the verdict, severity ranks the content, your content rules decide first and triage decides the rest. reasons records the path taken.
- Checks and usageA check is the unit every plan counts. See what one moderation costs, when the monthly allowance resets and how to read your usage over the API.
- Rate limitsEach API key may send a set number of requests a minute, by plan. Every response says where the key stands, and a 429 says how long to wait.
Something missing or unclear on this page? Write to support@useinfrared.com.