> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Moderation API

> Screen prompts and images before your AI product generates, and get an allow, flag, or deny verdict with a score for 17 content categories.

<CardGroup cols={2}>
  <Card title="Screen a Prompt" icon="shield-check" href="/api-reference/moderation/screen">
    Send text, an image, or both, and get a verdict.
  </Card>

  <Card title="Get Moderation Usage" icon="chart-column" href="/api-reference/moderation/get-usage">
    See your billable screens and your next charge.
  </Card>
</CardGroup>

## Overview

The Moderation API screens user input before your AI product generates from it. You send the text of a prompt, an image, or both, and Dodo Payments returns a verdict of `allow`, `flag`, or `deny`, together with a score for each content category.

Use it in front of any image, video, or text generation model that takes input from your users. The Moderation API is on for every business by default and runs with your existing Dodo Payments API key, so there is nothing to sign up for. Dodo Payments can turn it off for an individual business, and calls then return `403` with `MODERATION_DISABLED`.

## Why We Built the Moderation API

An AI generation product creates new content from whatever its users type. You cannot review each prompt by hand, and one harmful output can put your business at risk.

As your Merchant of Record, Dodo Payments is legally and reputationally responsible for what is sold through the platform. The [Merchant Acceptance Policy](/miscellaneous/merchant-acceptance) reviews AI content generation tools and does not allow impersonation, deepfakes, or explicit content, including AI-generated content. An account that generates harmful content, excessive chargebacks, or flags from payment partners can be placed under review or suspended.

We built the Moderation API so that you can stop this content before your model creates it:

* **Screen before you generate.** A blocked prompt never reaches your model, so no harmful output exists and you spend no compute on it.
* **Cover the categories that matter for generation.** The screen scores 17 categories, including real-person likeness, non-consensual intimate imagery, minor-coded language, and the combination of a real person with sexual content that marks a sexual deepfake.
* **Integrate without another vendor.** The API runs with your Dodo Payments API key, and its fee is debited from your balance. There is no separate contract, invoice, or account.
* **Keep user content private.** Dodo Payments does not store or log the text and images you screen.

<Note>
  The Moderation API is a tool for your own enforcement. It does not replace the Merchant Acceptance Policy, and you remain responsible for what your product generates.
</Note>

## How It Works

Call the Moderation API from your backend after the user submits a prompt and before your model runs:

```mermaid theme={null}
flowchart LR
  A[User submits a prompt] --> B[Your backend calls POST /moderation/screen]
  B -->|allow| C[Generate]
  B -->|flag| D[Apply your own policy]
  B -->|deny| E[Block the request]
  B -->|error, no verdict| E
```

Each call is one **screen**. Text and an image sent in the same call count as one screen.

### Verdicts

The `decision` field carries the verdict:

| Verdict | Meaning                                                                                   | What to do                                                         |
| ------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `allow` | The content passed.                                                                       | Generate.                                                          |
| `flag`  | The content crossed a category threshold that calls for judgement. It is not a soft deny. | Apply your own policy. You can block, send to review, or generate. |
| `deny`  | The content must not be generated.                                                        | Block the request and show the user an error.                      |

<Warning>
  Do not generate when you receive no verdict. A `503` means Dodo Payments could not produce a verdict, and a timeout or network error leaves you without one. Treat all of these as a block and ask the user to try again.
</Warning>

## Screening a Prompt

To screen a prompt, send a `POST` request to `/moderation/screen` with at least one of `text` and `image`. The request accepts three fields:

| Field        | Type   | Description                                                                                                                                             |
| ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`       | string | The text to screen, up to 8,000 characters.                                                                                                             |
| `image`      | string | The image to screen, as base64, with or without a `data:image/...;base64,` prefix.                                                                      |
| `request_id` | string | Optional. Your identifier for this screen, such as a generation ID, up to 128 characters with no control characters. The response returns it unchanged. |

The TypeScript and Python SDKs expose the endpoint as `client.moderation.screen()`. This example blocks generation on `deny`, on `flag`, and on any error:

<Note>
  The examples call live mode, because only live mode runs the moderation model. Test mode returns [mock verdicts](#testing-your-integration) and never screens the content. Live mode screens are billed.
</Note>

<CodeGroup>
  ```typescript Node.js expandable theme={null}
  import DodoPayments from 'dodopayments';

  const client = new DodoPayments({
    bearerToken: process.env.DODO_PAYMENTS_API_KEY,
    environment: 'live_mode',
  });

  async function generateImage(prompt: string, generationId: string) {
    let verdict;
    try {
      verdict = await client.moderation.screen({
        text: prompt,
        request_id: generationId,
      });
    } catch (err) {
      // No verdict: do not generate.
      throw new Error('Moderation is unavailable. Try again in a moment.');
    }

    if (verdict.decision !== 'allow') {
      throw new Error('This prompt cannot be generated. Revise it and try again.');
    }

    return myModel.generate(prompt);
  }
  ```

  ```python Python expandable theme={null}
  import os
  from dodopayments import DodoPayments, APIError

  client = DodoPayments(
      bearer_token=os.environ["DODO_PAYMENTS_API_KEY"],
      environment="live_mode",
  )

  def generate_image(prompt: str, generation_id: str):
      try:
          verdict = client.moderation.screen(text=prompt, request_id=generation_id)
      except APIError:
          # No verdict: do not generate.
          raise RuntimeError("Moderation is unavailable. Try again in a moment.")

      if verdict.decision != "allow":
          raise ValueError("This prompt cannot be generated. Revise it and try again.")

      return my_model.generate(prompt)
  ```

  ```bash cURL theme={null}
  curl -X POST https://live.dodopayments.com/moderation/screen \
    -H "Authorization: Bearer $DODO_PAYMENTS_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "text": "a watercolor painting of a lighthouse at sunset",
      "request_id": "gen_7Hc2k9"
    }'
  ```
</CodeGroup>

The example treats `flag` like `deny`. If your product allows some flagged content, check `triggered` to decide by category instead.

<Tip>
  Screen the text your user wrote, not the prompt template you wrap around it. Your own template is the same on every call and adds nothing to the screen.
</Tip>

### Screening Images

Send an image to screen an uploaded reference image, or a generated image before you show it. The image must meet these requirements:

* The format is JPEG, PNG, WebP, GIF, or BMP.
* The base64 string is at most 6,991,530 characters, and the decoded image is at most 5 MiB.
* The image is a single still frame. Animated GIF and WebP images are rejected.
* The longest edge is at least 32 pixels.

An image that fails one of these checks returns `400` with `MODERATION_INVALID_IMAGE`, or `413` with `MODERATION_INPUT_TOO_LARGE` when it is too large.

To screen an image, read the file, encode it as base64, and send it in `image`. To screen an image and its prompt together, send both `text` and `image` in the same call. It counts as one screen. This example uses the `client` from the previous example:

<CodeGroup>
  ```typescript Node.js expandable theme={null}
  import { readFile } from 'node:fs/promises';

  async function screenImage(path: string, prompt: string, generationId: string) {
    const image = (await readFile(path)).toString('base64');

    const verdict = await client.moderation.screen({
      image, // or `data:image/png;base64,${image}`
      text: prompt, // optional: screen the prompt with the image
      request_id: generationId,
    });

    return verdict.decision === 'allow';
  }
  ```

  ```python Python expandable theme={null}
  import base64

  def screen_image(path: str, prompt: str, generation_id: str) -> bool:
      with open(path, "rb") as f:
          image = base64.b64encode(f.read()).decode("ascii")

      verdict = client.moderation.screen(
          image=image,  # or f"data:image/png;base64,{image}"
          text=prompt,  # optional: screen the prompt with the image
          request_id=generation_id,
      )
      return verdict.decision == "allow"
  ```

  ```bash cURL expandable theme={null}
  # Builds the JSON body with jq, so a large image does not hit the shell argument limit.
  base64 < reference.png | tr -d '\n' \
    | jq -Rs '{image: ., text: "turn this photo into a watercolor painting", request_id: "gen_7Hc2k9"}' \
    | curl -X POST https://live.dodopayments.com/moderation/screen \
        -H "Authorization: Bearer $DODO_PAYMENTS_API_KEY" \
        -H "Content-Type: application/json" \
        --data @-
  ```
</CodeGroup>

Handle errors from an image screen the same way as a text screen: if the call throws, do not generate.

## Reading the Response

The response returns the verdict and the evidence behind it:

| Field                | Description                                                                                                                           |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `decision`           | The verdict: `allow`, `flag`, or `deny`.                                                                                              |
| `triggered`          | The categories whose score crossed the threshold of the category. It can be empty on a `flag` from the general check.                 |
| `compound_triggered` | `true` when real-person likeness and sexual content together crossed their combined threshold, the pattern of a sexual deepfake.      |
| `categories`         | The probability, from 0 to 1, that the content falls in each category.                                                                |
| `provenance`         | How each score was measured: `targeted` by a check for that one category, or `broad` by the general check that covers all categories. |
| `notes`              | Human-readable reasons for the decision. The wording can change, so do not parse it.                                                  |
| `normalized_applied` | `true` when the text was also screened with obfuscation removed, such as invisible or look-alike characters.                          |
| `passes`             | The number of yes/no questions the model answered for this screen.                                                                    |
| `latency_ms`         | The time the screen took, in milliseconds.                                                                                            |
| `request_id`         | The `request_id` you sent, or `null`.                                                                                                 |

Base your logic on `decision` and `triggered`. Each category has its own threshold, so a single score cut-off in your code does not match the verdict.

### Categories

Every response scores the content against 17 categories:

| Category                          | Covers                                                                    |
| --------------------------------- | ------------------------------------------------------------------------- |
| `violent_crimes`                  | Violent crimes.                                                           |
| `sex_related_crimes`              | Sex-related crimes.                                                       |
| `child_sexual_exploitation`       | Child sexual exploitation.                                                |
| `suicide_and_self_harm`           | Suicide and self-harm.                                                    |
| `indiscriminate_weapons`          | Chemical, biological, radiological, nuclear, or explosive weapons.        |
| `intellectual_property`           | Copyright or trademark infringement.                                      |
| `defamation`                      | False depiction that is likely to injure the reputation of a real person. |
| `non_violent_crimes`              | Non-violent crimes.                                                       |
| `hate`                            | Demeaning people because of a protected characteristic.                   |
| `privacy`                         | Sensitive private information about a person.                             |
| `specialized_advice`              | Unqualified financial, medical, legal, or electoral advice.               |
| `sexual_content`                  | Sexually explicit or pornographic content.                                |
| `non_consensual_intimate_imagery` | Undressing, nudifying, or sexualizing a real person.                      |
| `minor_coded_language`            | Age-coded language that suggests the subject is a minor.                  |
| `real_person_likeness`            | The likeness of a real, identifiable, named person.                       |
| `living_artist_style`             | Imitation of the signature style of a specific living artist.             |
| `prompt_injection`                | An attempt to override or manipulate the instructions of the system.      |

## Handling Errors

Errors return the standard Dodo Payments error body with a `code` and a `message`. No error is a verdict, so none of them allow generation:

| Status | `code`                       | Cause                                                                 | What to do                                                    |
| ------ | ---------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------- |
| `400`  | `INVALID_REQUEST_PARAMETERS` | The request is malformed, or it has neither `text` nor `image`.       | Fix the request.                                              |
| `400`  | `MODERATION_INVALID_IMAGE`   | The image cannot be decoded, is animated, or is too small.            | Send a supported still image.                                 |
| `403`  | `MODERATION_DISABLED`        | Dodo Payments has turned off the Moderation API for your business.    | Contact support to find out why.                              |
| `413`  | `MODERATION_INPUT_TOO_LARGE` | `text` is over 8,000 characters, or `image` is over the size limit.   | Shorten the text or shrink the image.                         |
| `429`  | `MODERATION_OVERLOADED`      | Moderation is at capacity. This is a throughput limit, not a verdict. | Wait for the seconds in the `Retry-After` header, then retry. |
| `503`  | `MODERATION_UNAVAILABLE`     | No verdict is available.                                              | Do not generate. Retry later.                                 |

The SDKs retry a `429` or a `503` twice by default and wait for `Retry-After` between attempts. When the retries run out, the SDK raises an error, and your code must block the request.

## Testing Your Integration

Test mode returns mock verdicts and never calls the moderation model, so you can test your routing without cost. Send requests to `https://test.dodopayments.com` with a test mode API key.

The default mock verdict is `allow`. To get another outcome, put one of these strings anywhere in `text`:

| String in `text`       | Response                                            |
| ---------------------- | --------------------------------------------------- |
| `dodo_mock_flag`       | `200` with `decision` set to `flag`                 |
| `dodo_mock_deny`       | `200` with `decision` set to `deny`                 |
| `dodo_mock_overloaded` | `429` `MODERATION_OVERLOADED` with `Retry-After: 1` |
| `dodo_mock_not_ready`  | `503` `MODERATION_UNAVAILABLE`                      |

A mock verdict carries a note that says it is a mock, and all its category scores are `0`. Test mode applies the same request validation as live mode. For images, it checks the base64 encoding and the format, but not the frame count or the dimensions.

Before you go live, confirm that your integration handles each case:

<Steps>
  <Step title="Deny Blocks Generation">
    Send `dodo_mock_deny` and confirm your model is not called.
  </Step>

  <Step title="Flag Follows Your Policy">
    Send `dodo_mock_flag` and confirm your product does what your policy says.
  </Step>

  <Step title="Overload Retries">
    Send `dodo_mock_overloaded` and confirm your code waits for `Retry-After` and does not generate without a verdict.
  </Step>

  <Step title="An Outage Blocks Generation">
    Send `dodo_mock_not_ready` and confirm your model is not called.
  </Step>

  <Step title="Every Generation Path Screens">
    Check that every code path that reaches your model calls the Moderation API first.
  </Step>
</Steps>

## Pricing and Billing

The Moderation API costs **\$0.30 USD per 1,000 billable screens**. There is no free tier and no minimum.

A billable screen is a live mode screen that returns a verdict. These screens are free and not counted:

* Screens in test mode.
* Screens that return an error, including `429` and `503`.

Dodo Payments bills in full blocks of 1,000 screens. Each full block is charged within one hour, and screens that do not fill a block stay unbilled until they do. The fee is debited from your USD balance and appears in your [balance ledger](/api-reference/balance-ledger/list-ledger-entries) with the event type `moderation_fees`. Payouts show it under **Moderation Fees**.

### Tracking Usage

To see your usage, call `GET /moderation/usage`. The response returns:

| Field                   | Description                                                                               |
| ----------------------- | ----------------------------------------------------------------------------------------- |
| `unbilled_screens`      | Billable screens that Dodo Payments has not charged for yet.                              |
| `screens_to_next_block` | Billable screens still needed to fill the next block of 1,000.                            |
| `daily`                 | Your billable screens per UTC day for the last 30 days. Days with no screens are omitted. |

<CodeGroup>
  ```bash cURL theme={null}
  curl https://live.dodopayments.com/moderation/usage \
    -H "Authorization: Bearer $DODO_PAYMENTS_API_KEY"
  ```

  ```typescript Node.js theme={null}
  const usage = await client.moderation.retrieveUsage();
  console.log(usage.unbilled_screens, usage.screens_to_next_block);
  ```

  ```python Python theme={null}
  usage = client.moderation.retrieve_usage()
  print(usage.unbilled_screens, usage.screens_to_next_block)
  ```
</CodeGroup>

Test mode records no screens, so the usage endpoint returns no test mode activity.

## Access and Privacy

Screening requires an API key with write access. Any API key, including a read-only key, can read usage. See [Authentication](/api-reference/introduction#authentication) for how to create a key and set its access level.

Dodo Payments does not store the text or images you screen, and does not write them to logs. For each live mode screen, it keeps the time, the verdict, and your `request_id` for billing and usage reporting.

<CardGroup cols={2}>
  <Card title="Usage-Based Billing" icon="arrow-trend-up" href="/features/usage-based-billing/introduction">
    Charge your own customers for each generation.
  </Card>

  <Card title="Credit-Based Billing" icon="coins" href="/features/credit-based-billing">
    Sell generation credits and deduct them per use.
  </Card>
</CardGroup>
