Skip to content

Moderate text in one call and images by webhook

Text and images go to the same endpoint: text returns 200 with the verdict, an image returns 202 and finishes by signed webhook or by polling.

  • REST and JSON under /v1
  • Secret keys, server to server
POST/v1/moderations
Sample data

TextAnswered in the same request

FlaggedReject
  1. POST{ "type": "text", "input": "Worst seller ever. You are a complete idiot." }
  2. 200{ "flagged": true, "recommendation": "reject", "triggered": ["toxicity"], … }

ImageAccepted, then finished by webhook

Screening
  1. POST{ "type": "image", "input": "https://example.com/1.jpg" }
  2. 202{ "status": "screening", "recommendation": null, … }
  3. POST https://hooks.example.com/infraredYour endpointNot sent yet
    Infrared-Event
    moderation.completed
    Infrared-Signature
    t=1790942400,v1=<64 hex characters>
    data.moderation
    { "flagged": false, "recommendation": "allow", … }

Sample data

Send one object, a whole form or a batch

POST /v1/moderations takes a single object or a whole form, and /v1/moderations/batch takes a list of either.

type
Takes text or image, and defaults to text.
fields
A whole form of 1 to 40 fields. Your own fields need a template, on Starter and up.
items
1 to 100 objects or forms in one batch call. Your plan sets the cap.
POST /v1/moderationsShell
curl https://api.useinfrared.com/v1/moderations \
  -H "Authorization: Bearer ir_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "text",
    "input": "You are a complete idiot.",
    "content_id": "review_18342",
    "author_id": "user_42",
    "metadata": { "source": "ios" }
  }'
No template is named, so the project's default runs. Until you set one, that is the built-in basic check, on every plan.
POST /v1/moderationsShell
curl https://api.useinfrared.com/v1/moderations \
  -H "Authorization: Bearer ir_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "listings",
    "fields": {
      "title": "Vintage oak dining table, seats six",
      "description": "Solid oak, light wear on one leg.",
      "price": 450,
      "photos": ["https://cdn.example.com/1.jpg"]
    }
  }'
With photos this answers 202. The finished result is one parent moderation of type form, with a full moderation for each text and image field.
POST /v1/moderations/batchShell
curl https://api.useinfrared.com/v1/moderations/batch \
  -H "Authorization: Bearer ir_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: import-2026-10-02-0001" \
  -d '{
    "items": [
      { "type": "text", "input": "Great seller, fast delivery.", "content_id": "review_1" },
      { "type": "text", "input": "You are a complete idiot.", "content_id": "review_2" }
    ]
  }'
A list of results in item order. An item the API refuses comes back as an error entry, and the others still run.

Retry a call without moderating twice

Send an Idempotency-Key with a moderation or a batch call, and a repeat with the same body within 24 hours returns the first response.

POST/v1/moderations
Sample data

Idempotency-Key:7b1f0c52-upload-18342

  1. First call

    The text is moderated and the response is stored.

    200 OK

  2. Same key, same body

    Nothing is moderated again.

    200 OKIdempotent-Replayed: true

  3. Same key, different body

    The call is refused and nothing is moderated.

    409 Conflictidempotency_key_reused

Sample data

Test keys run the full pipeline for free

An ir_test_ key works on the same project and templates as a live key, uses no checks and marks the response test: true.

  • The queue and webhooks still runA test call is recorded as a moderation, can enter the review queue and fires your webhooks.

  • Trust levels do not moveA test call is not counted in an author's history.

  • Counted apartUsage records test traffic separately from live.

Key
Sample data

Request header

Authorization: Bearer ir_test_…

Runs the full pipeline, never consumes checks.

Checks used by this call
0
Checks this month
8,420 of 10,000
FlaggedRejectTest
200 OK, trimmedJSON
{  "id": "mod_1B7lY4kzM9n2Qx8sRt3wVu",  "status": "completed",  "flagged": true,  "recommendation": "reject",  "usage": { "checks": 0, … },  "dry_run": false,  "test": true,  …}

Sample data

One error shape to branch on

Every error is the same envelope: a stable code, a message, details and the request id.

invalid_api_key
401The key is malformed, unknown, revoked or expired.
plan_limit
403A template other than the basic check on Free, or a batch over the plan's cap.
validation_error
422A field is missing, mistyped or unknown. Each one is listed in details.
Error responses
Sample data
402 Payment Required
{"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_5f0c2b7e9a4d4c1b8e3f6a7d9c0b1e2f"}}
429 Too Many Requests
X-RateLimit-Limit: 60X-RateLimit-Remaining: 0X-RateLimit-Reset: 1790942460Retry-After: 23{"error": {"code": "rate_limited","message": "Rate limit exceeded for this API key","details": [],"request_id": "req_9d3a7c1e5b2f4a8c6e0d1b3f5a7c9e2d"}}

X-Request-IdOn every response. A moderation keeps it, and the dashboard shows it as Request.

Sample data

Start moderating your users' content

Create an API key, send your first request and get a verdict back.