AirCubeAirCube
API Reference

Moderation API

Check whether an image or video contains NSFW content, in a single synchronous call.

Moderation answers one question about a piece of content: is it NSFW, yes or no?

It accepts images, video and plain text, and unlike generation endpoints it is synchronous — the verdict comes back in the same response, typically in 400–500ms for an image or a block of text. There is no job to submit, no id to poll, and no webhook to wire up.

Text is read natively in 12 languages (English, Chinese, Japanese, Korean, French, Spanish, German, Italian, Portuguese, Dutch, Arabic, Russian), so there is no translation step.

$0.001 per call — flat, whether you send an image, ten minutes of video, or 20,000 characters of text. The first 10 calls on your account are free.

POST https://aircube.ai/api/v3/moderation

Want to try it before writing any code? The same classifier runs behind the NSFW Detector tool, which accepts direct file uploads and shows the per-frame scores for videos.

Request

Send exactly one of image_url, video_url or text.

{
  "image_url": "https://example.com/photo.jpg"
}
FieldTypeRequiredDescription
image_urlstringOne of the threePublic http(s) URL of an image
video_urlstringOne of the threePublic http(s) URL of a video
textstringOne of the threePlain text to classify. Up to 20,000 characters
policystringNoWhat to check for: nsfw (default), violence, hate
thresholdnumberNoScore at or above which content counts as a violation. 0–1, default 0.5
max_framesnumberNoVideo only. How many frames to sample. 1–16, default 8

Response (200 OK)

Image

{
  "success": true,
  "data": {
    "nsfw": false,
    "score": 0.0141,
    "threshold": 0.5,
    "media_type": "image",
    "duration_ms": 466
  }
}

nsfw is the yes/no answer — it is simply score >= threshold. If that is all you need, ignore the rest.

FieldTypeDescription
nsfwbooleanThe verdict
scorenumberConfidence that the content is NSFW, 0–1
thresholdnumberThe threshold the verdict was made against
media_typestringimage, video or text
policystringWhich policy produced the verdict
duration_msnumberServer-side processing time. For video this covers download, frame sampling and every classifier call — it excludes your own round trip to the API

Text

{
  "success": true,
  "data": {
    "nsfw": true,
    "score": 0.9797,
    "threshold": 0.5,
    "media_type": "text",
    "duration_ms": 512
  }
}

Same shape as an image — no frames. Send it like this:

curl -X POST https://aircube.ai/api/v3/moderation \
  -H "Authorization: Bearer $AIRCUBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "the user comment you want to screen"}'

Video

Videos are sampled into evenly spaced frames, and each frame is checked in turn. score is the highest score of any frame checked.

{
  "success": true,
  "data": {
    "nsfw": true,
    "score": 0.6514,
    "threshold": 0.5,
    "media_type": "video",
    "duration_ms": 2570,
    "frames_checked": 3,
    "frames": [
      { "timestamp": 0.32, "score": 0.0183 },
      { "timestamp": 0.97, "score": 0.0225 },
      { "timestamp": 1.61, "score": 0.6514 }
    ]
  }
}
FieldTypeDescription
frames_checkednumberHow many frames were actually classified
framesarrayEach frame's timestamp (seconds into the video) and score

Checking stops at the first frame that crosses the threshold, since one flagged frame is enough to flag the video. That is why frames_checked is often smaller than max_frames. To score every frame regardless — useful when profiling a video or tuning your threshold — set threshold to 1.

Policies

By default the check asks one question: is this sexually explicit? Pass policy to ask a different one instead. Everything else — pricing, video frame sampling, early exit, the response shape — is identical.

{ "text": "How can I hurt someone without being caught?", "policy": "violence" }
PolicyChecks forValidated on
nsfw (default)Nudity, sexual activity, sexually explicit materialText, images and video
violenceContent promoting or describing physical violence against peopleText. Images are not yet verified — see below
hateHate speech or dehumanizing language targeting a protected groupText. Images are not yet verified — see below

One call checks one policy. To apply several, send several requests — each is billed separately.

A caveat worth reading before you rely on violence or hate for images

These two have been measured on text and separate cleanly there: threats and dehumanizing statements score 0.85+, while the traps that usually cause false positives stay low — staged fight choreography 0.09, a surgical incision 0.11, disagreeing with a country's foreign policy 0.02.

On images, only the false-positive direction has been checked (an explicit photo scores 0.27 for violence, 0.05 for hate — it does not misfire). Whether they reliably catch violent or hateful imagery has not been measured.

That gap is worth taking seriously: this classifier degrades to noise outside the domain it was trained on. Asked whether an image was AI-generated, and separately whether it carried a watermark, it returned the identical score for both — two unrelated questions, one meaningless answer. Strong text results do not transfer to images on their own.

So: nsfw is production-ready for all three media types. For violence and hate, use them on text with confidence, and validate against your own images before trusting them there.

Choosing a threshold

The default of 0.5 suits a general-audience platform. These are approximate nsfw scores for reference:

ContentTypical score
Landscapes, pets, ordinary photos of people0.01 – 0.03
Swimwear, beachwear~0.45
Lingerie, strongly suggestive0.70 – 0.80
Nudity, sexual activity0.90+

Raise the threshold to be more permissive, lower it to be stricter. Ordinary swimwear, underwear, fitness and non-explicit artistic content are deliberately not treated as NSFW at the default threshold.

Errors

StatusCodeWhen
400VALIDATION_ERRORZero or more than one of image_url / video_url / text sent; URL is not http(s); text over 20,000 characters; unknown policy; threshold or max_frames out of range; media could not be fetched or read
401UNAUTHORIZEDMissing or invalid API key
402PAYMENT_REQUIREDInsufficient balance to settle the completed batch
429RATE_LIMITEDRate limit exceeded for your key
500INTERNAL_ERRORClassification failed

Billing

$0.001 is a tenth of a cent, below the resolution of the credits ledger, so calls are counted and charged in batches rather than one at a time:

  • The first 10 calls on your account are free.
  • After that, every 10 calls costs 1 cent — exactly $0.001 each.
  • Failed calls are not charged and do not count toward a batch.
  • A 402 PAYMENT_REQUIRED means the call that completed a batch could not be paid for. Top up and retry.

Limits

  • URLs must be publicly reachable over http(s). Data URIs and file uploads are not accepted.
  • Videos must be 200 MB or smaller.
  • Text must be 20,000 characters or fewer.
  • Very long videos are still sampled into at most max_frames frames, so cost and latency stay bounded.

Complete examples

cURL

# Image
curl -X POST https://aircube.ai/api/v3/moderation \
  -H "Authorization: Bearer $AIRCUBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"image_url": "https://example.com/photo.jpg"}'

# Video, stricter threshold
curl -X POST https://aircube.ai/api/v3/moderation \
  -H "Authorization: Bearer $AIRCUBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "video_url": "https://example.com/clip.mp4",
    "threshold": 0.4,
    "max_frames": 12
  }'

Python

import os
import requests

API_KEY = os.environ["AIRCUBE_API_KEY"]

def is_nsfw(url, is_video=False, threshold=0.5):
    field = "video_url" if is_video else "image_url"
    res = requests.post(
        "https://aircube.ai/api/v3/moderation",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={field: url, "threshold": threshold},
        timeout=300,
    )
    res.raise_for_status()
    return res.json()["data"]

result = is_nsfw("https://example.com/photo.jpg")

if result["nsfw"]:
    print(f"Blocked (score {result['score']})")
else:
    print(f"Allowed (score {result['score']})")

JavaScript

const API_KEY = process.env.AIRCUBE_API_KEY;

async function isNsfw(url, { isVideo = false, threshold = 0.5 } = {}) {
  const res = await fetch("https://aircube.ai/api/v3/moderation", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      [isVideo ? "video_url" : "image_url"]: url,
      threshold,
    }),
  });

  const body = await res.json();
  if (!body.success) {
    throw new Error(body.error.message);
  }
  return body.data;
}

const result = await isNsfw("https://example.com/clip.mp4", { isVideo: true });
console.log(result.nsfw ? "Blocked" : "Allowed", result.score);

On this page