> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://fastpix.com/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://fastpix.com/docs/_mcp/server.

# Gate uploads before publishing

## What you're building

A publishing pipeline where playback is closed by default and opened by a decision rather than by the end of processing.

Media is created with a private access policy, so it can only be played through a short-lived signed URL that your application issues. Once processing finishes, your application starts moderation on the ready asset. When the result arrives, your rules decide what happens: media that meets your policy gets a public playback ID and becomes viewable, media that does not stays private while a reviewer looks at it or while the contributor is asked to re-cut it.

The pipeline is event-driven end to end. Two webhooks carry it: one says the media is ready to be moderated, the other says the moderation result is in.

The contributor never waits on a person for the common case, and no asset becomes publicly playable without passing your policy first.

\


## Prerequisites

* A FastPix account with an active workspace ([Activate your account](/getting-started/activate-your-account))
* Your **Access Token ID** and **Secret Key**
* A signing key if you plan to issue signed playback URLs ([Generate JWTs for secure media](/video-security/generate-jwts-for-secure-media))
* Thresholds chosen for your audience. See [Content moderation](/in-video-ai/detect-nsfw-content-and-profanity) for what the scores mean.

\


## Create the media with playback closed

Set `accessPolicy` to `private` so the asset cannot be played without a signed token. Nothing about this asset is public yet, and nothing becomes public until you decide it should be.

```bash
curl --request POST \
  --url https://api.fastpix.com/v1/on-demand \
  --user '{Access_Token}:{Secret_Key}' \
  --header 'Content-Type: application/json' \
  --data '{
    "inputs": [
      {
        "type": "video",
        "url": "https://static.fastpix.com/fp-sample-video.mp4"
      }
    ],
    "metadata": {
      "uploaderId": "user_8842",
      "reviewState": "pending"
    },
    "accessPolicy": "private"
  }'
```

Storing your own `reviewState` in `metadata` gives you a field to query later and keeps the publishing state with the asset rather than only in your database.

For uploads that come from a device rather than a URL, use [Upload media from device](/video-on-demand-api/input-video/direct-upload-video-media) and set the same `accessPolicy` inside `pushMediaSettings`.

\


## Start moderation when the media is ready

Moderation runs on a processed asset, so it cannot start at upload time. Wait for the `video.media.ready` webhook, which fires once the media has finished processing and is playable, then trigger moderation on it.

The trigger takes an optional configuration body (see [Moderation configuration](/in-video-ai/getting-started-with-moderation#moderation-configuration)). By default, with no body, both the visual and the spoken track are analyzed, and the result reports which of them produced findings in `moderation.modalities`.

```bash
curl --request POST \
  --url https://api.fastpix.com/v1/ai/{mediaId}/moderation \
  --user '{Access_Token}:{Secret_Key}'
```

The response confirms the job started:

```json
{
  "success": true,
  "data": {
    "id": "c695988b-ff84-42ae-bb21-10f284fedb0e"
  }
}
```

Trigger this from your `video.media.ready` handler rather than from the upload response. The upload returning does not mean the asset is processed, and moderation on an unready asset will not run.

\


## Read the moderation result

Moderation runs in the background. Subscribe to the `video.media.ai.moderation.ready` webhook rather than polling, so your pipeline reacts as soon as the analysis completes.

If you need to fetch the result directly, call [Get video moderation results](/in-video-ai-api/content-moderation/get-media-ai-moderation).

```bash
curl --request GET \
  --url https://api.fastpix.com/v1/ai/{mediaId}/moderation \
  --user '{Access_Token}:{Secret_Key}'
```

\


## Apply your policy to the result

Read the result at two levels. Use `moderation.scores` for the whole-media decision, and `moderation.segments` when you want to know exactly which moments caused it. For the full response shape, see [Understand the moderation response](/in-video-ai/getting-started-with-moderation#understand-the-moderation-response).

Map the outcome to one of three actions:

| Outcome                                         | Action                                                     |
| ----------------------------------------------- | ---------------------------------------------------------- |
| All category scores below your review threshold | Publish. Issue a public playback ID.                       |
| Any score inside your review band               | Hold. Keep the media private and create a review task.     |
| Any score above your automatic-action threshold | Reject. Keep the media private and notify the contributor. |

Use separate thresholds for video and audio findings, since the two tracks do not carry equal confidence. Keep the policy in your own application rather than inferring it from the response, so you can retune thresholds later without changing your integration.

\


## Open playback for approved media

Publishing is the act of [creating a public playback ID](/video-on-demand-api/playback/create-media-playback-id). Until this call succeeds, the asset has no public route to a viewer.

```bash
curl --request POST \
  --url https://api.fastpix.com/v1/on-demand/{mediaId}/playback-ids \
  --user '{Access_Token}:{Secret_Key}' \
  --header 'Content-Type: application/json' \
  --data '{
    "accessPolicy": "public"
  }'
```

The response returns the playback ID your player uses:

```json
{
  "success": true,
  "data": {
    "id": "b331e0d8-bef4-4ad2-8760-757fdb2818b7",
    "accessPolicy": "public"
  }
}
```

> **Note:** Once the access policy is public, play the video with its HLS URL `https://stream.fastpix.com/{playback_id}.m3u8`, replacing `{playback_id}` with the playback ID (`id`) returned above.

\


## Hold, review, or revoke

Media that did not clear needs no further call. With no public playback ID it is already unavailable, which is why closing playback at creation rather than opening it at creation is the safer default.

For media in your review band, issue a short-lived signed playback URL so a reviewer can watch it without it becoming public. See [Generate JWTs for secure media](/video-security/generate-jwts-for-secure-media). Give the reviewer the flagged categories and the segment time ranges alongside the video, so they can jump to the moment instead of watching the whole asset.

If something was published and later needs pulling, delete the playback ID. The media and its moderation history stay intact, so the decision can be appealed or reversed.

```bash
curl --request DELETE \
  --url 'https://api.fastpix.com/v1/on-demand/{mediaId}/playback-ids?playbackId={playbackId}' \
  --user '{Access_Token}:{Secret_Key}'
```

\


## What to watch for

* **Set a deadline on the pending state.** Moderation runs in the background, so build a timeout into your own pipeline and route anything still pending when it expires to human review, rather than leaving it held.
* **Moderation runs on a ready asset.** It cannot be enabled at upload time. Trigger it from your `video.media.ready` handler, not from the upload response, and build your pending state around media readiness.
* **Sampling favours shorter media.** Accuracy is strongest on short-form video. Evaluate results against your own catalog before relying on automatic publishing for long assets.
* **Audio findings depend on language.** Audio moderation runs on the transcript, so it covers the [supported audio moderation languages](/in-video-ai/detect-nsfw-content-and-profanity#supported-languages-for-audio-moderation). Content outside those languages is still screened visually.
* **Give contributors a specific reason.** The segments carry start and end times. A contributor told which seconds failed usually re-cuts and resubmits, where a generic rejection tends to end the relationship.

\


#### Frequently asked questions

Create the media with `accessPolicy` set to `private` and do not create a public playback ID. The asset can then only be played through a signed URL that your application issues, so it has no public route to a viewer until you decide to publish it.

From your `video.media.ready` webhook handler. AI features run on processed media, so moderation cannot start at upload time and a trigger sent before the asset is ready will not run. The upload call returning is not the same signal as the media being ready.

Delete its playback ID. The media and its moderation results are retained, so the decision can be reviewed or reversed later. Deleting the media itself removes that option.

Route it to review. Reserve automatic rejection for scores above a high threshold, and use the review band for everything uncertain. Tune the width of that band against your own reviewer capacity.

\


## What's next

* Repair a flagged asset instead of rejecting it: [Fix flagged content and publish](/in-video-ai/fix-flagged-content-and-publish).
* Send uncertain results to a person: [Review flagged media](/in-video-ai/review-flagged-media).
* Understand the categories and scores behind the decision: [Content moderation](/in-video-ai/detect-nsfw-content-and-profanity).