Analyze a video for inappropriate content with the Mux Robots API. Use this workflow when your platform accepts user-generated video and you must review uploads.
Analyze a video for inappropriate content. Mux samples thumbnails from the video and scores each one for sexual and violent content. You can configure the thresholds that determine whether content gets flagged. See the Moderate API referenceAPI for the full endpoint specification. See Mux Robots pricing for unit costs.
moderate jobcurl https://api.mux.com/robots/v0/jobs/moderate \
-H "Content-Type: application/json" \
-X POST \
-d '{
"parameters": {
"asset_id": "YOUR_ASSET_ID",
"thresholds": {
"sexual": 0.7,
"violence": 0.8
}
}
}' \
-u ${MUX_TOKEN_ID}:${MUX_TOKEN_SECRET}This request is asynchronous. The POST returns immediately with the job in pending status and does not include results. We strongly recommend listening for the robots.job.moderate.completed webhook — the payload contains the full completed job, so no follow-up API call is needed. If webhooks aren't an option, you can poll GET /robots/v0/jobs/moderate/{JOB_ID} with the id from the response until the status is completed.
| Parameter | Type | Description |
|---|---|---|
asset_id | string | Required. The Mux asset ID of the video to moderate. |
language_code | string | Language code for transcript analysis on audio-only assets. Defaults to en. |
thresholds | object | Score thresholds that determine whether content is flagged. |
thresholds.sexual | number | Score threshold (0.0-1.0) for sexual content. Defaults to 0.7. Lower the value to be more strict (e.g. 0.5 to flag borderline content). |
thresholds.violence | number | Score threshold (0.0-1.0) for violent content. Defaults to 0.8. Lower the value to be more strict. |
sampling_interval | integer | Interval in seconds between sampled thumbnails. Minimum 5. For example, 10 samples a frame every 10 seconds. Good when you want consistent coverage regardless of video length. |
max_samples | integer | Maximum number of thumbnails to sample. Samples are distributed evenly across the video with the first and last frames pinned. For example, 20 on a 10-minute video samples roughly every 30 seconds. Good when you want predictable cost per job. |
output_steering | object | Optional controls that restrict where moderation runs on the asset. |
output_steering.scope | object | Time window in seconds on the asset timeline, with start_time and end_time. Moderation is restricted to media within this window, while returned timestamps remain absolute asset timestamps. Omit start_time to begin at the asset start, and end_time to continue through the asset end. |
on_flagged | object | Optional action to take automatically when exceeds_threshold is true. When omitted, the job only reports scores and takes no action. |
on_flagged.action | string | The action to perform. Currently the only supported value is delete_playback_ids, which deletes every playback ID on the asset, making it unplayable while preserving the underlying asset so it can be re-published or reviewed. |
By default, the workflow samples a thumbnail every 10 seconds. Use sampling_interval to override this; the minimum is 5 seconds. Smaller intervals give denser coverage at higher cost; larger intervals are cheaper but may miss brief moments of flagged content.
For example, with sampling_interval: 10, a 60-second video produces 6 samples — at 0, 10, 20, 30, 40, and 50 seconds.
If you set max_samples alongside sampling_interval, the cap takes over once the interval would produce more samples than allowed. See the parameter descriptions above.
The defaults (sexual: 0.7, violence: 0.8, one sample every 10 seconds) are a reasonable starting point. The right configuration depends on your platform's risk model: who can upload, how quickly content becomes public, who your audience is, and the impact to your business of a missed detection compared to an incorrect takedown.
Open UGC platforms where anyone can sign up and publish immediately carry the most risk and warrant the strictest settings. Platforms with vetted uploaders or a publishing delay can lean on the defaults. Closed platforms (internal video, course content, fitness apps) are mostly guarding against account compromise and mistakes, and can prioritize cost instead.
| Platform profile | Suggested starting point |
|---|---|
| Open UGC, instant publishing | sexual: 0.6, violence: 0.6, sampling_interval: 5, and a human review queue for scores near the threshold |
| Vetted uploaders, or a review delay before publishing | The defaults: sexual: 0.7, violence: 0.8, sampling_interval: 10 |
| Closed or internal platform | sexual: 0.8, violence: 0.9, max_samples: 20 |
For zero-tolerance audiences, such as platforms for children, go stricter still, like the thresholds in the strict moderation example below. Whatever you pick, compare your first few hundred jobs against the calls your human reviewers actually make, then adjust.
A thumbnail's score is how confident the model is that the category is present in that frame, not how extreme the content is. Clearly visible but mild nudity can score higher than graphic violence in a dark, partially obscured shot. That distinction matters when you pick thresholds:
sexual from 0.7 to 0.5 doesn't redefine what counts as sexual content; it flags frames the model is less certain about. You'll catch more borderline content, and more false positives.0.9 isn't "worse" than a 0.75; the model is just more sure. Use the score to decide whether a human looks at the video, and let the human judge severity against your content policy.on_flagged to unpublish content automatically, pair it with thresholds high enough that you can tolerate acting without review.Each sample costs 100 units ($0.001); a job's total cost is just its sample count. The two controls described in Sampling behavior determine that count in opposite ways:
sampling_interval fixes coverage; cost scales with duration. One sample every N seconds means no stretch of video longer than N seconds goes unchecked, whether the upload is two minutes or two hours. Use it when you want a coverage guarantee and can let spend vary per video.max_samples fixes cost; coverage thins as videos get longer. The same N samples are spread evenly across the video, with the first and last frames pinned, so a long upload costs the same as a short one but leaves wider gaps between samples. Use it when you need a predictable worst-case cost per video, especially when upload durations vary widely.| Configuration | 10-minute video | 1-hour video |
|---|---|---|
sampling_interval: 5 | 120 samples ($0.12) | 720 samples ($0.72) |
sampling_interval: 10 (the default) | 60 samples ($0.06) | 360 samples ($0.36) |
max_samples: 20 | 20 samples ($0.02), one every ~30 seconds | 20 samples ($0.02), one every ~3 minutes |
You can also combine them: set sampling_interval for the coverage you want and max_samples as a spending cap that takes over on long videos. For example, sampling_interval: 10 with max_samples: 60 gives full 10-second coverage on videos up to 10 minutes, and caps any longer video at $0.06.
How dense you need to go depends on what you're screening for:
max_samples cap, or a 30-60 second interval) still hits it with high probability.Rather than paying for dense sampling on every upload, one option is to run a cheap first pass on everything and spend more only where the results are ambiguous:
max_samples: 20) and your normal thresholds.max_scores lands just under a threshold (within 0.2 of it, for example), run a second moderate job with sampling_interval: 5. Use output_steering.scope to restrict the second pass to the region around the suspicious samples in thumbnail_scores instead of re-scanning the whole video.{
"parameters": {
"asset_id": "YOUR_ASSET_ID",
"sampling_interval": 5,
"output_steering": {
"scope": {
"start_time": 120,
"end_time": 300
}
}
}
}Most uploads on most platforms are innocuous, so the bulk of your library clears at the first-pass price of $0.02 per video, and only the ambiguous few need a second job.
No sampling interval catches everything, and no threshold gets every judgment right. Treat automated moderation as triage, not a verdict: route borderline scores to a human review queue, and give users a way to report content that slips through. At minimum, that's in-product reporting and a dedicated abuse address. See contact mechanisms and the other operational strategies in the Moderate video content guide.
The outputs object is included in the job once its status is completed. You'll receive it on the robots.job.moderate.completed webhook (recommended), or you can fetch it with GET /robots/v0/jobs/moderate/{JOB_ID}. It contains:
| Field | Type | Description |
|---|---|---|
thumbnail_scores | array | Per-thumbnail moderation scores, each with sexual and violence fields (0.0-1.0). Also includes a time field (seconds) for video assets; absent for transcript moderation. |
max_scores | object | Highest scores across all thumbnails, with sexual and violence fields. |
exceeds_threshold | boolean | true if any category's max score exceeds its configured threshold. |
flagged_action | object | Present only when on_flagged was set and exceeds_threshold was true. Records the action taken, with action (the action performed) and deleted_playback_ids (the playback IDs removed from the asset). |
This is the payload delivered to the robots.job.moderate.completed webhook, and the same shape you get from GET /robots/v0/jobs/moderate/{JOB_ID}:
{
"data": {
"id": "rjob_def456",
"workflow": "moderate",
"status": "completed",
"units_consumed": 1,
"parameters": {
"asset_id": "YOUR_ASSET_ID",
"thresholds": {
"sexual": 0.7,
"violence": 0.8
}
},
"outputs": {
"thumbnail_scores": [
{ "time": 0.0, "sexual": 0.01, "violence": 0.02 },
{ "time": 5.0, "sexual": 0.03, "violence": 0.05 }
],
"max_scores": {
"sexual": 0.03,
"violence": 0.05
},
"exceeds_threshold": false
}
}
}Moderation works by sampling thumbnail frames from your video. You can control how many frames are analyzed with sampling_interval or max_samples. More samples give better coverage but increase processing time and cost.
If content safety is critical for your platform, lower the thresholds and increase the sample density:
{
"parameters": {
"asset_id": "YOUR_ASSET_ID",
"thresholds": {
"sexual": 0.3,
"violence": 0.4
},
"max_samples": 50
}
}With lower thresholds, even mildly suggestive or mildly violent content will cause exceeds_threshold to return true, giving you a signal to flag the video for human review before publishing.
By default, a moderate job only reports scores — it never changes your asset. Set on_flagged to have the workflow act automatically when exceeds_threshold is true:
{
"parameters": {
"asset_id": "YOUR_ASSET_ID",
"thresholds": {
"sexual": 0.3,
"violence": 0.4
},
"on_flagged": {
"action": "delete_playback_ids"
}
}
}When the content exceeds a threshold, delete_playback_ids removes every playback ID on the asset, making it immediately unplayable while preserving the underlying master asset so you can review it and re-publish later by creating a new playback ID.
The action is best-effort and non-fatal: if a deletion fails, the job still completes with its moderation scores rather than erroring. The outcome is reported back in the job output as flagged_action:
{
"outputs": {
"max_scores": {
"sexual": 0.82,
"violence": 0.10
},
"exceeds_threshold": true,
"flagged_action": {
"action": "delete_playback_ids",
"deleted_playback_ids": ["abc123...", "def456..."]
}
}
}Deleting playback IDs is irreversible — the IDs themselves cannot be restored, and any cached or shared URLs using them stop working. If this moderate job runs as part of a Directive that targets the same asset with other workflows, deleting playback IDs can break sibling workflows that need a playback ID.