# CLAUDE
Source: https://docs.moderationapi.com/CLAUDE
# Mintlify documentation
## Working relationship
* You can push back on ideas-this can lead to better documentation. Cite sources and explain your reasoning when you do so
* ALWAYS ask for clarification rather than making assumptions
* NEVER lie, guess, or make up information
## Project context
* Format: MDX files with YAML frontmatter
* Config: docs.json for navigation, theme, settings
* Components: Mintlify components
## Content strategy
* Document just enough for user success - not too much, not too little
* Prioritize accuracy and usability of information
* Make content evergreen when possible
* Search for existing information before adding new content. Avoid duplication unless it is done for a strategic reason
* Check existing patterns for consistency
* Start by making the smallest reasonable changes
## docs.json
* Refer to the [docs.json schema](https://mintlify.com/docs.json) when building the docs.json file and site navigation
## Frontmatter requirements for pages
* title: Clear, descriptive page title
* description: Concise summary for SEO/navigation
## Writing standards
* Second-person voice ("you")
* Prerequisites at start of procedural content
* Test all code examples before publishing
* Match style and formatting of existing pages
* Include both basic and advanced use cases
* Language tags on all code blocks
* Alt text on all images
* Relative paths for internal links
## Git workflow
* NEVER use --no-verify when committing
* Ask how to handle uncommitted changes before starting
* Create a new branch when no clear branch exists for changes
* Commit frequently throughout development
* NEVER skip or disable pre-commit hooks
## Do not
* Skip frontmatter on any MDX file
* Use absolute URLs for internal links
* Include untested code examples
* Make assumptions - always ask for clarification
* Expose underlying dependencies, technology, or internal APIs. Describe user-facing behavior only — no vendor or model names (OpenAI, GPT, Anthropic, Claude, etc.), no infra (Redis, Postgres, xxHash), no frameworks or SDKs (tRPC, React, Tailwind, Zustand, Prisma), and no internal endpoints or procedure names. Only the public REST endpoints under `/api-reference/*` should be referenced by name. Frame limits and constraints around reliability and UX, not cost or token counts.
# Manage team
Source: https://docs.moderationapi.com/account/adding-team-members
How to add team members to your organization
Visit the [team management page](https://dash.moderationapi.com/team) to add team members to your organization.
You can add team members by entering their email address. They will receive an email invitation to join your organization.
## Roles
There are three roles in an organization:
* **Owner**: The owner has full access to the organization and can manage team members and billing. Only one owner can exist per organization and cannot be removed or changed.
* **Admin**: The admin has full access to the organization and can manage team members and billing.
* **Developer**: The developer can access and change the projects, models, and review queues, but cannot manage team members or billing.
* **Moderator**: The moderator can access and use the review queues, but cannot manage team members, billing, projects, or models. You can limit the access of moderators to specific queues.
# Quota usage
Source: https://docs.moderationapi.com/account/quota-usage
Understanding how quota usage works and how to manage it is important for managing your costs.
## Included quota
All Moderation API plans include a certain amount of quota. This quota is used when you submit content for moderation.
The included quota is different for each plan. See the [pricing page](https://moderationapi.com/pricing) for more information.
Custom plans can be created with a different quota for use cases where you need more quota, so please [reach out](https://moderationapi.com/sales) if you need more quota.
## Exceeding the included quota
By default, the API will block requests when you've exceeded the included quota and repond with a `429 status code`. If you want to continue submitting content, you can **upgrade** your plan or enable **pay-as-you-go**.
### Upgrade to a higher plan
If you upgrade to a higher plan, your quota will immediately be reset to the included quota for the new plan.
Upgrade to a higher plan in the dashboard here.
### Pay-as-you-go
If you enable pay-as-you-go, you will be billed for the quota you use on top of your included quota. This is useful if you want to avoid being blocked when you exceed the included quota.
Set up pay-as-you-go limits in the dashboard here.
#### When is pay-as-you-go billed?
Pay-as-you-go is billed monthly at the same time as your subscription.
## Viewing quota usage
You can view your quota usage in the dashboard.
View your quota usage in the dashboard here.
## When does the quota reset?
Your quota resets at the beginning of your billing cycle.
For example, if you have a monthly plan, your quota will reset on the day that you started your plan. You can also see the start date of your billing cycle in the dashboard on the [billing page](https://dash.moderationapi.com/billing).
## How is the quota usage calculated?
The quota usage is calculated based on the number of requests you make to the API, which models you use, and the plan your account is on.
You can see the quota usage for a request in the response.
* Pre-built text models use 1 quota unit per request no matter how many models you use.
* Pre-built image models use 3 quota units per image.
* Custom models use 1 quota unit per request per model.
* Some exceptions might apply.
#### Example:
* Your project is configured with 5 pre-built text models and 1 custom model.
* 5 pre-built text models = 1 quota unit per request
* 1 custom model = 1 quota unit per request
* Total quota usage = 2 quota units per request
* Submit 10 requests to the API = 10 x 2 quota units = 20 quota units
## Please reach out if you need a custom quota solution
If your use case does not fit into the default price model, please [reach out](https://moderationapi.com/sales) and we can discuss a custom plan or another solution.
# Manage plan
Source: https://docs.moderationapi.com/account/upgrading
Get more quota and features by upgrading your plan
If you need more quota or features, you can upgrade your plan at any time. Additional quota will be available immediately.
## How to change plan
Visit the [billing page](https://dash.moderationapi.com/billing) and click on the "Manage billing" button. You will be redirected to the billing portal.
Here you can:
* Update your plan
* Cancel your subscription
* Change your payment method
* Download your invoices
# Author actions
Source: https://docs.moderationapi.com/actions/author-actions
Block, suspend, and manage authors with built-in and custom moderation actions.
Moderation actions provide direct moderation controls for managing problematic users in your community.
Common scenarios for moderation actions:
* **Block persistent violators** identified through [trust levels](/users/trust-levels) or [fraud detection](/users/fraud-detection)
* **Remove spam accounts** posting promotional content repeatedly
* **Suspend users** to send clear signals about acceptable behavior
Actions can be executed manually from the [user dashboard](https://dash.moderationapi.com/project/latest/authors) or programmatically via the [Author API](/api-reference/author). They complement content moderation by addressing the user behind the content, helping you stop persistent abuse and enforce community standards.
## Built-in actions
The system provides three essential moderation actions for comprehensive account management:
Permanently prevents a user from submitting content for moderation.
**When to use:**
* Spam accounts with no legitimate content
* Users repeatedly violating major community guidelines
* Accounts identified as malicious through fraud detection
* Coordinated attacks or bot networks
**Technical behavior:**
* User status set to `"blocked"`
* All future content submissions are rejected
* `block.until` field set to `null` (permanent)
* `block.reason` stores the reason provided
Temporarily prevents a user from submitting content for a specified period.
**When to use:**
* First-time serious violations that warrant a cooling-off period
* Users who might reform with temporary consequences
* Escalating enforcement before permanent blocks
* Accounts needing investigation time
**Technical behavior:**
* User status set to `"suspended"`
* Content submissions rejected until suspension ends
* `block.until` field set to suspension end time (timestamp)
* `block.reason` stores the reason provided
* Automatic reinstatement when period expires
Removes existing blocks or suspensions, restoring normal access.
**When to use:**
* Appeals that have been approved
* Mistaken blocks that need correction
* Policy changes requiring user reinstatement
* Early release from suspensions for good behavior
**Technical behavior:**
* User status reset to `"enabled"`
* Content submission access restored
* Block fields cleared from user record
## Action customization
You can customize how actions appear and behave:
**Value customization:**
* **Predefined values**: Add custom dropdown options like "Spam", "Harassment", "Fraud", "Policy Violation"
* **Free text option**: Enable "Other" option that shows a text input field for custom reasons
* **Combined approach**: Mix predefined categories with free text for flexibility
### Adding custom actions
Beyond built-in actions, create custom author-level actions for specialized workflows that show up alongside standard actions.
**Use cases for custom actions:**
* **Warning systems**: Formal warnings that don't restrict access
* **Verification requirements**: Require additional account verification
* **Feature restrictions**: Limit specific platform features
* **Escalation triggers**: Automatically escalate to senior moderators
To create custom actions, choose "Author level action" when creating a new action in the dashboard. These appear alongside built-in actions on user detail pages.
## Executing actions
1. Navigate to the user detail page in the [user dashboard](https://dash.moderationapi.com/project/latest/authors)
2. Select the appropriate action (Block, Suspend, or Enable)
3. Choose from predefined reason options or select "Other" for free text
4. For suspensions: select duration from dropdown or set custom end time
5. Confirm the action
**Reason field importance:**
* **Required for documentation** and potential appeals
* **Visible to moderators** in user timeline
* **Returned in API responses** for application logic
* **Helps identify patterns** in violation types
**Timeline**
All moderation actions are automatically tracked in the user's timeline in the dashboard:
* **Action history**: Complete chronological record of all actions taken
* **Moderator attribution**: Which team member executed each action
* **Reason logging**: Full context for every moderation decision
* **Audit trail**: Comprehensive record for compliance and appeals
This timeline helps moderators understand user history and make informed decisions about future actions.
```javascript theme={"theme":"nord"}
// Block a user permanently
const blockResponse = await fetch(
"https://api.moderationapi.com/v1/actions/execute",
{
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
actionKey: "AUTHOR_BLOCK",
authorIds: ["{authorId}"],
value: "Repeated spam violations",
}),
}
);
// Suspend a user temporarily
const suspendResponse = await fetch(
"https://api.moderationapi.com/v1/actions/execute",
{
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
actionKey: "AUTHOR_BLOCK_TEMP",
authorIds: ["{authorId}"],
value: "Harassment of other users",
duration: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
}),
}
);
// Unblock a user (remove block/suspension)
const unblockResponse = await fetch(
"https://api.moderationapi.com/v1/actions/execute",
{
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
actionKey: "AUTHOR_UNBLOCK",
authorIds: ["{authorId}"],
value: "Appeal approved",
}),
}
);
```
See the [Author API documentation](/api-reference/author) for complete endpoint details.
## Technical implementation
Moderation actions modify several fields returned in API responses, allowing seamless integration with your application logic.
### Status fields
Monitor user status through API responses:
```javascript theme={"theme":"nord"}
{
"id": "user123",
"status": "suspended", // "enabled", "blocked", "suspended"
"block": {
"until": 1705320000000, // timestamp in ms, null for permanent blocks
"reason": "Harassment of other users"
}
}
```
### Content submission integration
The system automatically handles blocked/suspended users when content is submitted to [moderation endpoints](/api-reference/moderate/analyze-text), but you can also check status proactively in your application by querying the user in the Author API.
### Webhooks
Receive real-time notifications when moderation actions are executed through [webhook integration](/actions/webhooks):
**Common webhook use cases:**
* **Sync with external systems** like customer support platforms
* **Trigger email notifications** to affected users
* **Update internal user databases** with moderation status
* **Log actions for compliance** and audit requirements
* **Alert security teams** about fraud-related blocks
Configure webhooks to receive instant notifications whenever blocks, suspensions, or other moderation actions are executed.
***
## FAQ
Moderation actions only prevent content submission for moderation. They don't affect content viewing unless you implement additional restrictions in your application using the status fields.
Existing content remains in your system and review queues. moderation actions
only prevent new submissions. You may want to review existing content from
blocked users separately.
The system provides the suspension information through API responses, but
displaying this to users depends on your application implementation. Use the
`user.block.until` and `user.block.reason` fields to show appropriate
messages.
Use the Enable action to reinstate users after successful appeals. The action
history timeline maintains a record of the original block and subsequent
reinstatement for transparency.
You can use the Enable action to end suspensions early, or apply a new Suspend
action to change the period. Each action creates a new timeline entry for full
audit tracking.
Yes, moderation actions complement these systems perfectly. High fraud risk scores or low trust levels often indicate when moderation actions are needed. The systems work together to provide comprehensive user management.
# Content actions
Source: https://docs.moderationapi.com/actions/content-actions
Reject, allow, and custom actions that run on individual queue items.
Content actions run against a single queue item. They're how moderators (and your application code) decide what happens to flagged content.
## Built-in actions
Two are always available:
* **Reject** — the item is rejected. Fires the `queue_item.rejected` [webhook event](/actions/webhooks).
* **Allow** — the item is approved. Fires the `queue_item.allowed` event.
## Custom actions
Beyond reject and allow, create custom content actions for any workflow you need — "Hide post", "Send to legal", "Escalate to senior moderator", "Republish to homepage". Custom actions appear in the queue alongside the built-ins and fire the generic `queue_item.action` webhook event.
We recommend using actions for things like:
* Publish approved content
* Remove rejected content
* Route items into dedicated views (see [Routing with views](#routing-with-views))
* Anything your moderation flow needs
Configure actions under [**Configure → Actions**](https://dash.moderationapi.com/project/latest/configure/actions).
## Resolving an item when an action runs
An action can be configured to resolve the item automatically when it runs, removing it from the queue. This saves moderators a step — they don't have to click "resolve" after taking an action.
## Adding a value
Actions can carry a value — a reason ("spam", "harassment"), a category, a duration, or any free-text input. Configure the available values per action:
* **Predefined values** — show a dropdown of options
* **Free text** — show a text input
* **Both** — predefined options plus an "Other" free-text fallback
The value is included in the [webhook payload](/actions/webhooks) so your application can branch on it.
## Routing with views
You can use actions to route content into dedicated [views](/review/content-views). Create a view with an **Action taken** filter for a specific action, and any item that gets that action will appear in the view automatically — this is how escalation and user-report workflows are built.
## Running actions programmatically
Trigger any action — built-in or custom — from your own systems via the [execute action endpoint](/api-reference/actions/execute-moderation-action).
## Integrating with your app
Pair actions with [webhooks](/actions/webhooks) to react in real time, or use one of our [platform plugins](https://moderationapi.com/integrations) for direct integrations with Discord, Slack, WordPress, and more.
# Actions
Source: https://docs.moderationapi.com/actions/overview
Actions are the verbs of moderation — what moderators (and automation) do to content and users.
An **action** is anything that can run against a piece of content or a user: rejecting a comment, blocking an author, sending a warning, removing a profile from your app. Each action is configurable, can carry a value (a reason, a category, a duration), and can fire a [webhook](/actions/webhooks) so your application reacts in real time.
Actions live in two flavors:
Run on individual queue items. Built-in **reject** and **allow**, plus any custom actions you create — "Hide post", "Send to legal", "Flag for review", etc.
Run on the user behind the content. Built-in **block**, **suspend**, and **enable**, plus custom author-level actions like warnings or feature restrictions.
## How actions are used
* **In the dashboard.** Moderators run actions from the queue or user detail pages. Custom actions appear alongside the built-ins.
* **Programmatically.** Call the [execute action endpoint](/api-reference/actions/execute-moderation-action) to run any action — built-in or custom — against content or authors from your own systems.
* **As webhook triggers.** Every action that runs fires a [webhook event](/actions/webhooks) (`queue_item.rejected`, `author.blocked`, `queue_item.action`, etc.) so you can sync state back to your application.
## Built-in vs. custom
Built-in actions cover the common cases — rejecting content, blocking authors, and so on. If they don't cover what you need, you can create custom actions to fit your workflow.
| | Built-in | Custom |
| ----------------- | ------------------------------------------------------- | ----------------------------------------------- |
| **Created by** | The system | You |
| **Examples** | Reject, allow, block, suspend, unblock | "Hide post", "Send warning email", "Escalate" |
| **Webhook event** | Dedicated event per action (e.g. `queue_item.rejected`) | Generic `queue_item.action` or `author.action` |
| **Configurable** | Possible values, queue visibility | Everything: name, level, values, queue behavior |
Configure actions under [**Configure → Actions**](https://dash.moderationapi.com/project/latest/configure/actions) in the dashboard.
## What's next
Reject, allow, and custom item-level actions.
Block, suspend, enable, and custom user-level actions.
Receive real-time events when actions run.
# Webhooks
Source: https://docs.moderationapi.com/actions/webhooks
Receive HTTP callbacks when moderation events happen — queue items resolved, authors blocked, custom actions performed, and more.
Webhooks let you react to moderation events in real time. Whenever an event you subscribe to fires, we'll `POST` a JSON payload to your URL. A single webhook can subscribe to **any combination of events** — you don't need a separate URL per event type.
Configure webhooks under [**Configure → Webhooks**](https://dash.moderationapi.com/project/latest/configure/webhooks) in the dashboard.
## How it works
1. Add a webhook in the dashboard, set its URL, and tick the events you care about.
2. Whenever any of those events fire, we POST the event to your URL.
3. Your endpoint returns a `2xx` within 5 seconds to acknowledge.
4. Anything else (timeout, `4xx`, `5xx`) triggers retries with exponential backoff — up to **5 attempts** total.
## Request headers
Every delivery includes:
| Header | Description |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| `webhook-version` | Payload envelope version. Currently always `v2`. |
| `webhook-event-id` | Stable event ID (matches `id` in the body). Use this to dedupe retries. |
| `modapi-signature` | HMAC-SHA256 signature of the raw request body. See [Verifying signatures](#verifying-signatures). |
| `User-Agent` | `ModAPI/1.0`. Useful for allow-listing in security tools — see [Troubleshooting](#troubleshooting). |
| `Content-Type` | `application/json`. |
## Payload envelope
Every event shares the same outer envelope. The event-specific data lives in `data.object`.
Stable event ID, prefixed with `evt_`. Identical across retries — use it to
dedupe.
The event type, e.g. `queue_item.rejected` or `author.blocked`. See the [event
catalog](#event-catalog).
Always `v2`.
ISO 8601 timestamp of when the event was emitted.
Wraps the event-specific payload.
The resource that triggered the event. The shape depends on `type` — see
each event below.
```json Envelope shape theme={"theme":"nord"}
{
"id": "evt_clxxx...",
"type": "queue_item.rejected",
"api_version": "v2",
"created": "2026-05-08T12:34:56.789Z",
"data": {
"object": {
/* event-specific resource */
}
}
}
```
Need typed payloads? The full OpenAPI schema for every event lives under
`components.schemas` in our [OpenAPI
spec](https://app.stainless.com/api/spec/documented/moderation-api/openapi.documented.yml)
— look for `WebhookEvent` (a discriminated union over `type`) and the
per-event schemas like `QueueItemRejectedEvent`, `AuthorBlockedEvent`, etc.
## Event catalog
| Event | Fires when | `data.object` |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| [`queue_item.resolved`](/api-reference/webhooks/queue-item-resolved) | A moderator marks a queue item as resolved (checked off the queue). | The item, plus optional `author` and `queue` references. |
| [`queue_item.action`](/api-reference/webhooks/queue-item-action) | A **custom** moderation action runs on a queue item. | The action that ran, with the related `item`, `author`, and `queue`. |
| [`queue_item.rejected`](/api-reference/webhooks/queue-item-rejected) | The built-in **reject** action runs on a queue item. | Same shape as `queue_item.action`. |
| [`queue_item.allowed`](/api-reference/webhooks/queue-item-allowed) | The built-in **allow** action runs on a queue item. | Same shape as `queue_item.action`. |
| [`author.blocked`](/api-reference/webhooks/author-blocked) | An author transitions to the `blocked` status. | The action that drove the transition, with the affected `author`. |
| [`author.unblocked`](/api-reference/webhooks/author-unblocked) | An author transitions back to `enabled`. May fire automatically when a temporary suspension expires. | Same shape as `author.blocked`. |
| [`author.suspended`](/api-reference/webhooks/author-suspended) | An author is suspended for a finite period. The nested `author.block.until` indicates when the suspension lifts. | Same shape as `author.blocked`. |
| [`author.updated`](/api-reference/webhooks/author-updated) | Public author fields (name, email, profile picture, metadata, etc.) change. Status transitions don't trigger this. | The updated `author`. |
| [`author.trust_level_changed`](/api-reference/webhooks/author-trust-level-changed) | An author's resolved trust level transitions to a new value. Doesn't fire for no-op recomputes. | The updated `author`. |
| [`author.action`](/api-reference/webhooks/author-action) | A custom action runs against an author. | Same shape as `author.blocked`. |
**Reject and allow are first-class events.** If you want to capture every
moderation action firing on a queue item, subscribe to `queue_item.action`
**and** `queue_item.rejected` **and** `queue_item.allowed`. Built-in actions
don't fire under `queue_item.action`.
## Routing by content type
Queue-item events carry the item's `meta_type` — a high-level classifier you set when submitting content (or inherit from the channel). Use it to fan a single webhook out to the right handler per entity:
| `meta_type` | Typical use |
| ----------- | --------------- |
| `profile` | User profiles |
| `message` | DMs / chat |
| `post` | Long-form posts |
| `comment` | Replies |
| `event` | Event listings |
| `product` | Marketplace |
| `review` | Ratings/reviews |
| `other` | Anything else |
```ts Webhook router theme={"theme":"nord"}
import type { WebhookEvent } from "@moderation-api/sdk";
const route = (event: WebhookEvent) => {
switch (event.type) {
case "queue_item.rejected":
switch (event.data.object.item?.meta_type) {
case "event":
return rejectEvent(event);
case "profile":
return rejectProfile(event);
case "review":
return rejectReview(event);
}
break;
case "queue_item.allowed":
switch (event.data.object.item?.meta_type) {
case "event":
return allowEvent(event);
case "profile":
return allowProfile(event);
case "review":
return allowReview(event);
}
break;
}
return null;
};
```
Set `meta_type` per submission via the `type` field on the [moderation
endpoint](/api-reference/moderate/submit-content), or configure it as the
default for a channel.
## Verifying signatures
Each delivery is signed with HMAC-SHA256 using your project's webhook secret. Find the secret under [API Keys in the dashboard](https://dash.moderationapi.com/project/latest/configure/api-keys).
To verify, compute `HMAC_SHA256(rawRequestBody, webhookSecret)` as a hex digest and compare it to the `modapi-signature` header. Always use a constant-time comparison.
```js Node.js (SDK) theme={"theme":"nord"}
import ModerationAPI from "@moderation-api/sdk";
// Reads MODAPI_SECRET_KEY by default
const client = new ModerationAPI();
export async function POST(request) {
const rawBody = await request.text();
const signatureHeader = request.headers.get("modapi-signature") ?? "";
// Verifies the signature with MODAPI_WEBHOOK_SECRET (or pass it explicitly
// as a third arg). Throws if the signature is invalid.
const event = client.webhooks.constructEvent(
Buffer.from(rawBody),
signatureHeader,
);
// `event` is typed as the WebhookEvent discriminated union — switching on
// `event.type` narrows `event.data.object` to the right resource.
switch (event.type) {
case "queue_item.rejected":
// event.data.object is the action_performed record
break;
case "author.blocked":
// event.data.object.author is the blocked author
break;
// ...
}
return Response.json({ received: true });
}
```
```js Node.js (manual) theme={"theme":"nord"}
import crypto from "crypto";
import { buffer } from "micro";
export const config = { api: { bodyParser: false } };
export default async function handler(req, res) {
const rawBody = (await buffer(req)).toString("utf8");
const signatureHeader = req.headers["modapi-signature"];
if (!signatureHeader) {
return res.status(400).json({ error: "Missing modapi-signature" });
}
const expected = crypto
.createHmac("sha256", process.env.MODAPI_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const sig = Buffer.from(signatureHeader, "utf8");
const dig = Buffer.from(expected, "utf8");
if (sig.length !== dig.length || !crypto.timingSafeEqual(sig, dig)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
switch (event.type) {
case "queue_item.rejected":
// handle reject
break;
case "author.blocked":
// handle block
break;
// ...
}
return res.json({ received: true });
}
```
```python Python (Flask) theme={"theme":"nord"}
import hmac
import hashlib
import os
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
SECRET = os.environ["MODAPI_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/moderation")
def moderation_webhook():
raw = request.get_data()
signature = request.headers.get("modapi-signature", "")
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
if event["type"] == "queue_item.rejected":
... # handle reject
elif event["type"] == "author.blocked":
... # handle block
return jsonify(received=True)
```
### Preventing replay attacks
The envelope `created` timestamp lets you reject events older than your tolerance window (e.g. 5 minutes). Combine that with the stable `webhook-event-id` header to dedupe retries — store recently-seen IDs and ignore repeats.
## Retries and delivery
* **Success:** any `2xx` response within 5 seconds closes the delivery.
* **Failure:** any non-`2xx`, timeout, or network error triggers a retry.
* **Limits:** up to **5 attempts** total, with exponential backoff between tries.
* **Permanent failure:** after the final attempt fails, we'll email the project's admin.
You can inspect every delivery attempt — request headers, payload, response status, and response body — under [**Events log**](https://dash.moderationapi.com/moderation/events) in the dashboard.
## Troubleshooting
### Check the events log
If webhooks aren't behaving as expected, the [events log](https://dash.moderationapi.com/moderation/events) is the first place to look. It shows every delivery attempt with full request and response details.
### Allow Moderation API through your firewall
Some hosting providers and security services block webhook traffic by default. Cloudflare's Bot Fight Mode, for example, will challenge the request instead of letting it through.
### Common blocking scenarios
* Bot Fight Mode / Super Bot Fight Mode is enabled
* WAF Managed Rules active
* Custom security rules
### Choose your solution based on your plan
**✅ Use Security Rules (Recommended)**
1. Go to your Cloudflare dashboard
2. Select your domain
3. Navigate to **Security → Security rules**
4. Create a custom rule:
* **Name**: "Moderation API Webhooks"
* **Field**: User Agent
* **Operator**: starts with
* **Value**: `ModAPI/`
* **Action**: Skip `All Super Bot Fight Mode Rules` and other rules that might interfere.
5. Make sure to place this rule before other rules
6. Deploy the rule
**❌ Security Rules won't work**
Bot Fight Mode cannot be bypassed using Skip actions in WAF custom rules or Page Rules. Skip, Bypass, and Allow actions only apply to Ruleset Engine rules.
**✅ Solutions:**
* **Option 1**: Upgrade to a Pro plan to use Super Bot Fight Mode (which can be bypassed)
* **Option 2**: Create a rule to allow Moderation API's IP address — see [Cloudflare's guide here](https://developers.cloudflare.com/waf/tools/ip-access-rules/create/), and reach out to support at [support@moderationapi.com](mailto:support@moderationapi.com) to get the IP address used for your account.
**Common providers with security blocking:**
* SiteDistrict
* DigitalOcean App Platform
* Vercel
* Netlify
* AWS CloudFront
* Azure Front Door
**General configuration steps:**
1. Access your provider's security/firewall settings
2. Look for "allow rules", "exceptions", or "bypass rules"
3. Add a User-Agent rule for `ModAPI/` (preferred)
4. Or contact support to get IP addresses allowlisted for your account
5. Save and deploy the configuration
**Where to find settings:**
* **Vercel**: Project settings → Security
* **Netlify**: Site settings → Build & deploy → Post processing
* **DigitalOcean**: App settings → Security section
* **AWS CloudFront**: WAF & Shield → Web ACLs
* **Azure Front Door**: Rules engine or WAF policies
Each provider uses different terminology. Look for "firewall rules", "security exceptions", or "allow rules" in your provider's documentation.
### Still having issues?
1. Check your hosting provider's security logs for blocked `ModAPI/` requests.
2. Verify your endpoint returns a `2xx` status code within 5 seconds.
3. Test with a minimal endpoint (just respond `200 OK`) to isolate the issue.
4. Make sure firewall allow rules don't conflict with deny rules earlier in the chain.
Need a hand? Email [support@moderationapi.com](mailto:support@moderationapi.com) and we'll help you configure or allowlist for your account.
# Get account details
Source: https://docs.moderationapi.com/api-reference/account/get-account-details
get /account
Get account details
# Create an action
Source: https://docs.moderationapi.com/api-reference/actions/create-an-action
post /actions
Create an action.
# Delete an action
Source: https://docs.moderationapi.com/api-reference/actions/delete-an-action
delete /actions/{id}
Delete an action and all of its webhooks.
# Execute an action
Source: https://docs.moderationapi.com/api-reference/actions/execute-an-action
post /actions/{actionId}/execute
Execute an action on a set of content items in a queue.
# Execute moderation action
Source: https://docs.moderationapi.com/api-reference/actions/execute-moderation-action
post /actions/execute
Execute a moderation action on one or more content items.
# Get an action
Source: https://docs.moderationapi.com/api-reference/actions/get-an-action
get /actions/{id}
Get an action by ID.
# List moderation actions
Source: https://docs.moderationapi.com/api-reference/actions/list-moderation-actions
get /actions
List all available moderation actions for the authenticated organization.
# Update an action
Source: https://docs.moderationapi.com/api-reference/actions/update-an-action
put /actions/{id}
Update an action.
# Authentication
Source: https://docs.moderationapi.com/api-reference/authentication
How to authenticate with the API
The Moderation API uses [API keys](/content-moderation/manage-api-key) to authenticate requests. Each project has its own API key. You can view and manage your API keys in the [dashboard](https://moderationapi.com/dashboard) -> Project -> Configure.
Your API keys carry many privileges, so be sure to keep them secure. Don't share your secret API keys in publicly accessible areas such as GitHub, client-side code, and so forth.
Authentication to the API is performed with the Authorization header when making a HTTP request. Provide your API key prefixed with `Bearer` as the value of the Authorization header.
You must make all API calls over HTTPS. Calls that you make over plain HTTP will fail. API requests without authentication will also fail.
We recommend creating two projects in your dashboard, one for development and
one for production. This way you can create tokens for both environments.
# Create a new author
Source: https://docs.moderationapi.com/api-reference/author/create-a-new-author
post /authors
Create a new author. Typically not needed as authors are created automatically when content is moderated.
# Delete an author
Source: https://docs.moderationapi.com/api-reference/author/delete-an-author
delete /authors/{id}
Delete a specific author
# Get author details
Source: https://docs.moderationapi.com/api-reference/author/get-author-details
get /authors/{id}
Get detailed information about a specific author including historical data and analysis
# List authors (Enterprise)
Source: https://docs.moderationapi.com/api-reference/author/list-authors
get /authors
Get a paginated list of authors with their activity metrics and reputation
# Update author details
Source: https://docs.moderationapi.com/api-reference/author/update-author-details
put /authors/{id}
Update the details of a specific author
# Create an action
Source: https://docs.moderationapi.com/api-reference/create-an-action
post /actions
Create an action.
# Delete an action
Source: https://docs.moderationapi.com/api-reference/delete-an-action
delete /actions/{id}
Delete an action and all of its webhooks.
# Errors
Source: https://docs.moderationapi.com/api-reference/errors
Learn about the error codes and messages that Moderation API returns.
Moderation API uses conventional HTTP response codes to indicate the success or failure of an API request. In general: Codes in the 2xx range indicate success. Codes in the 4xx range indicate an error that failed given the information provided (e.g., a required parameter was omitted or is formatted incorrectly, etc.). Codes in the 5xx range indicate an error with Moderation API's servers.
| Error Code | Meaning |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| 400 | Bad Request -- The request was unacceptable, often due to missing a required parameter. |
| 401 | Unauthorized -- No valid API key provided. |
| 403 | Forbidden -- The API key doesn't have permissions to perform the request, or your quota is used and you need to upgrade. |
| 404 | Not Found -- The specified resource could not be found. |
| 405 | Method Not Allowed -- You tried to access a resource with an invalid method. |
| 429 | Too Many Requests -- See [rate limits](/api-reference/rate-limits) |
| 500 | Internal Server Error -- We had a problem with our server. Try again later. |
| 503 | Service Unavailable -- We're temporarily offline for maintenance. Please try again later. |
## Response signature
Error responses are returned with a JSON object with the following signature:
The error code. `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`,
`METHOD_NOT_ALLOWED`, `TOO_MANY_REQUESTS`, `INTERNAL_SERVER_ERROR`,
`SERVICE_UNAVAILABLE`
A human-readable message providing more details about the error.
An array of issues that caused the error. For example input fields that are
invalid.
## Handling errors
Our API can return errors for many reasons, such as a authentication errors, and network unavailability. We recommend writing code that gracefully handles all possible API errors to avoid making your own service unavailable.
# Execute moderation action
Source: https://docs.moderationapi.com/api-reference/execute-an-action
post /actions/execute
Execute a moderation action on one or more content items.
```bash cURL theme={"theme":"nord"}
curl -X POST 'https://api.moderationapi.com/v1/actions/execute' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"contentIds": [
"60c9e1c0e4e7e1001c7a0e1e",
"60c9e1c0e4e7e1001c7a0e1f"
],
"actionId": "suspend-author",
"value": "Spam"
}'
```
```javascript Node.js theme={"theme":"nord"}
import ModerationAPI from "@moderation-api/sdk";
// Uses environment variable MODAPI_SECRET_KEY
const moderationApi = new ModerationAPI();
// Or pass key explicitly
// const moderationApi = new ModerationAPI({ secretKey: '' });
await moderationApi.actions.execute({
contentIds: ["60c9e1c0e4e7e1001c7a0e1e", "60c9e1c0e4e7e1001c7a0e1f"],
actionId: "suspend-author",
value: "Spam",
});
```
```ruby Ruby theme={"theme":"nord"}
require 'moderation_api'
ModerationApi.configure do |config|
config.access_token = ''
end
api = ModerationApi::QueueActionsApi.new
api.actions_execute(
content_ids: [
'60c9e1c0e4e7e1001c7a0e1e',
'60c9e1c0e4e7e1001c7a0e1f'
],
action_id: 'suspend-author',
value: 'Spam'
)
```
```python Python theme={"theme":"nord"}
import requests
url = "https://api.moderationapi.com/v1/actions/execute"
headers = {
"Authorization": "Bearer ",
"Content-Type": "application/json"
}
payload = {
"contentIds": [
"60c9e1c0e4e7e1001c7a0e1e",
"60c9e1c0e4e7e1001c7a0e1f"
],
"actionId": "suspend-author",
"value": "Spam"
}
response = requests.post(url, headers=headers, json=payload)
```
```php PHP theme={"theme":"nord"}
",
"Content-Type: application/json"
);
$data = array(
"contentIds" => array(
"60c9e1c0e4e7e1001c7a0e1e",
"60c9e1c0e4e7e1001c7a0e1f"
),
"actionId" => "suspend-author",
"value" => "Spam"
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
?>
```
```java Java theme={"theme":"nord"}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
String url = "https://api.moderationapi.com/v1/actions/execute";
String requestBody = """
{
"contentIds": [
"60c9e1c0e4e7e1001c7a0e1e",
"60c9e1c0e4e7e1001c7a0e1f"
],
"actionId": "suspend-author",
"value": "Spam"
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer ")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```
```go Go theme={"theme":"nord"}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
url := "https://api.moderationapi.com/v1/actions/execute"
data := map[string]interface{}{
"contentIds": []string{
"60c9e1c0e4e7e1001c7a0e1e",
"60c9e1c0e4e7e1001c7a0e1f",
},
"actionId": "suspend-author",
"value": "Spam",
}
jsonData, _ := json.Marshal(data)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer ")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result)
}
```
# Get an action
Source: https://docs.moderationapi.com/api-reference/get-an-action
get /actions/{id}
Get an action by ID.
# Introduction
Source: https://docs.moderationapi.com/api-reference/introduction
The Moderation API is organized around REST. Our API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.
## Just getting started?
Check out our quickstart guide to get up and running.
Get set up with Moderation API in 5 minutes.
## Not a developer?
If you're not a developer, you can still use the API. We have many integrations and no-code solutions that allows you to create projects and moderate content without writing a single line of code.
See our integrations and no-code solutions.
# List moderation actions
Source: https://docs.moderationapi.com/api-reference/list-actions
get /actions
List all available moderation actions for the authenticated organization.
# Submit content
Source: https://docs.moderationapi.com/api-reference/moderate/submit-content
POST /moderate
Submit text, image, video, audio, or object content for moderation analysis.
Submit content for moderation and receive analysis results immediately. Supports text, image, object, video, and audio content types.
For detailed usage examples and content metadata options, see [Submitting content](/content-moderation/submit-content).
# Real-time voice stream
Source: https://docs.moderationapi.com/api-reference/moderate/voice-stream
GET /stream
Open a WebSocket to moderate live voice audio in real time and receive a verdict per spoken utterance.
Moderate live voice and call audio over a WebSocket. You send a `start` frame, stream `media` frames as audio arrives, then `stop`; the server transcribes speech and returns a moderation verdict for each finalized utterance.
For the full walkthrough and code examples, see [Real-time voice](/content-moderation/real-time-voice).
# Rate limits
Source: https://docs.moderationapi.com/api-reference/rate-limits
Learn about API rate limits and how to work with them.
Different plans have different rate limit tiers based on your subscription plan. You can view your organization's current rate limit tier in your [dashboard](https://dash.moderationapi.com/usage).
To request an increased rate limit, please [contact our sales team](https://moderationapi.com/sales).
## Rate limit headers
The API returns rate limit information in the response headers for every request:
| Header | Description |
| ----------------------- | ------------------------------------------------------------------------------------- |
| `X-RateLimit-Limit` | The maximum number of requests you can make in the current time window |
| `X-RateLimit-Remaining` | The number of requests remaining in the current time window |
| `X-RateLimit-Reset` | Unix timestamp (in seconds) when your rate limit will reset |
| `Retry-After` | Number of seconds to wait before retrying (only included when rate limit is exceeded) |
### Example response headers
```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1699564800
```
## Handling rate limits
A basic technique for integrations to gracefully handle limiting is to watch for `429` status codes and build in a retry mechanism. The retry mechanism should follow an exponential backoff schedule to reduce request volume when necessary.
When you exceed your rate limit, the API will return a `429` error with a message indicating how long to wait before retrying. Use the `Retry-After` header to determine when to make your next request.
### Example error response
When rate limited, you'll receive a response like:
```json theme={"theme":"nord"}
{
"error": "Rate limit exceeded. Try again in 10 seconds."
}
```
The response will include the `Retry-After` header indicating how many seconds to wait.
## Checking your rate limit
You can check your current rate limit in your [dashboard under usage](https://dash.moderationapi.com/usage).
# Get a queue
Source: https://docs.moderationapi.com/api-reference/review-queues/get-a-queue
get /queue/{id}
Get a queue
# Get queue items
Source: https://docs.moderationapi.com/api-reference/review-queues/get-queue-items
get /queue/{id}/items
Get paginated list of items in a moderation queue with filtering options
# Get queue statistics
Source: https://docs.moderationapi.com/api-reference/review-queues/get-queue-statistics
get /queue/{id}/stats
Get detailed statistics about a moderation queue including review times, action counts, and trends
# Resolve a queue item
Source: https://docs.moderationapi.com/api-reference/review-queues/resolve-a-queue-item
post /queue/{id}/items/{itemId}/resolve
Mark a queue item as resolved with a specific moderation action
# Unresolve a queue item
Source: https://docs.moderationapi.com/api-reference/review-queues/unresolve-a-queue-item
post /queue/{id}/items/{itemId}/unresolve
Mark a previously resolved queue item as unresolved/pending
# Update an action
Source: https://docs.moderationapi.com/api-reference/update-an-action
put /actions/{id}
Update an action.
# Author action
Source: https://docs.moderationapi.com/api-reference/webhooks/author-action
openapi.json webhook author.action
Fired when a custom action is performed on an author.
# Author blocked
Source: https://docs.moderationapi.com/api-reference/webhooks/author-blocked
openapi.json webhook author.blocked
Fired when an author transitions to the blocked status.
# Author suspended
Source: https://docs.moderationapi.com/api-reference/webhooks/author-suspended
openapi.json webhook author.suspended
Fired when an author is temporarily suspended.
# Author trust level changed
Source: https://docs.moderationapi.com/api-reference/webhooks/author-trust-level-changed
openapi.json webhook author.trust_level_changed
Fired when an author's resolved trust level transitions to a new value.
# Author unblocked
Source: https://docs.moderationapi.com/api-reference/webhooks/author-unblocked
openapi.json webhook author.unblocked
Fired when an author transitions back to the enabled status.
# Author updated
Source: https://docs.moderationapi.com/api-reference/webhooks/author-updated
openapi.json webhook author.updated
Fired when an author's profile or metadata changes.
# Queue item action
Source: https://docs.moderationapi.com/api-reference/webhooks/queue-item-action
openapi.json webhook queue_item.action
Fired when a custom action is performed on a queue item.
# Queue item allowed
Source: https://docs.moderationapi.com/api-reference/webhooks/queue-item-allowed
openapi.json webhook queue_item.allowed
Fired when a queue item is allowed.
# Queue item rejected
Source: https://docs.moderationapi.com/api-reference/webhooks/queue-item-rejected
openapi.json webhook queue_item.rejected
Fired when a queue item is rejected.
# Queue item resolved
Source: https://docs.moderationapi.com/api-reference/webhooks/queue-item-resolved
openapi.json webhook queue_item.resolved
Fired when a queue item is resolved.
# Add words to wordlist
Source: https://docs.moderationapi.com/api-reference/wordlist/add-words-to-wordlist
post /wordlist/{id}/words
Add words to an existing wordlist
# Get embedding status
Source: https://docs.moderationapi.com/api-reference/wordlist/get-embedding-status
get /wordlist/{id}/embedding-status
Get the current embedding progress status for a wordlist
# Get wordlist
Source: https://docs.moderationapi.com/api-reference/wordlist/get-wordlist
get /wordlist/{id}
Get a specific wordlist by ID
# List wordlists
Source: https://docs.moderationapi.com/api-reference/wordlist/list-wordlists
get /wordlist
List all wordlists for the authenticated organization
# Remove words from wordlist
Source: https://docs.moderationapi.com/api-reference/wordlist/remove-words-from-wordlist
delete /wordlist/{id}/words
Remove words from an existing wordlist
# Update wordlist
Source: https://docs.moderationapi.com/api-reference/wordlist/update-wordlist
put /wordlist/{id}
Update a wordlist
# Managing your casebook
Source: https://docs.moderationapi.com/casebook/managing
Import decisions you already have, resolve disagreements, forget cases, and audit what the casebook settles
## The ledger
The **Cases** table on the Casebook page lists everything the casebook has learned.
Search it, or filter by **Ruling** (Allowed / Rejected), **Label**, or **Topic** —
including *No topic*. Each row shows the content, the ruling, the labels, who decided
it, and **Times applied** — how many items that one decision has settled. Open a case
to see the full content and jump to the item it was decided on.
## Importing cases
A casebook doesn't have to start from zero. **Import** on the Casebook page offers
two sources:
* **Use past review decisions.** Replays the last 90 days of your review-queue
decisions — each human allow or reject becomes a case. Decisions the casebook
already holds are skipped.
* **Upload a CSV.** Bring content and rulings from another tool. You pick the content
column, and map your own ruling values to *allow* or *reject* — nothing is guessed
for you. If the file has no ruling column, you can apply one ruling to every row.
Imports run in the background — close the wizard and keep working; the toolbar shows
a progress light while a run is live. Rows that can't be imported are counted and
reported without stopping the rest, and imported cases spend a short time learning
before they start matching content.
## Disagreements
Two cases that rule opposite ways on similar content block the casebook from ruling
on content like them — it applies neither until someone picks which ruling stands.
The **Disagreements** card counts them, and **Resolve** steps you through each group:
* **Should be allowed** / **Should be rejected** — one choice settles the whole
group. The losing side stops applying, and the casebook rules on content like this
again.
* **Keep both rulings** — every case stands. Repeats of each message follow their own
ruling, and new similar content still goes to review.
* **Keep as is** — hold an individual case out of the group decision.
## Forgetting a decision
Deleting a case makes the casebook forget it: similar content goes back to being
judged by your policies and rules alone. Before you confirm, the dialog shows how
much weight the case carries — how many items it has decided. Items it already
decided keep their outcome; deleting only changes the future.
## Auditing what it settles
The queue's [filters](/review/content-views) ask two casebook questions that combine:
* **Casebook** — what it said: *Casebook allowed*, *Casebook rejected*, or *No
casebook match*.
* **Casebook vs severity** — where it stood against severity triage: *Casebook
overruled* when they disagreed, *Casebook agreed* when they said the same.
Set both to narrow. *Casebook allowed* plus *Casebook overruled* is every item
severity triage would have held back that the casebook let through — the false
positives your moderators would otherwise re-approve by hand. **Handled for you** on the
Casebook page opens the same slice, and it's the one worth a periodic look once the
casebook is settling work for you: it's your team's own past judgement being applied,
so check that you still agree with it.
# Casebook
Source: https://docs.moderationapi.com/casebook/overview
The casebook remembers your team's review decisions and settles similar content automatically
Review teams decide the same things over and over. A rejected scam template, an
approved profile update, a corrected resubmission — each comes back with small edits
and gets reviewed again from scratch.
The casebook is your project's memory of review decisions. Every human allow or
reject in [review](/review/overview) is remembered as a **case**, and when new
content arrives that reads like something your team already decided, the casebook
applies the same ruling — so nobody reviews it again.
## How cases are created
Every human **Allow** or **Reject** in the review inbox becomes a case
automatically. An action that neither allows nor rejects is not added, and only
humans set precedent: automated decisions and author actions like block or warn
don't become cases.
When a [lookup](/casebook/rulings#test-your-casebook) finds nothing, **Add this
as a case** lets you teach a decision directly — paste the content, choose Allow
or Reject. Similar content follows from then on.
Seed a new casebook from the last 90 days of your review history, or upload a
CSV of content and rulings from another tool. See
[importing cases](/casebook/managing#importing-cases).
## What a case holds
A case is a snapshot of one decision: the content, the **ruling** — always allow or
reject — the **labels** your policies flagged at the time, the channel, who decided
it and when, and **times applied** — how many later items this case has settled. From
the case you can open the original item it was decided on.
A new case spends a short moment being committed to memory before it can match
content — the ledger marks these and tells you how many are left.
## How to enable
The casebook is on by default: new projects remember review decisions, and new
channels start with the casebook rule enabled. Two settings control it:
* **Configure → Casebook** turns the casebook on or off for the whole project. Off
means nothing is remembered and no lookups run.
* The **Casebook lookup** rule in each channel's [rules](/content-moderation/rules)
decides whether the casebook may settle content on that channel. Turn it off where
you want the casebook to watch and learn but not decide.
Applying rulings is available on the Growth plan and above. Remembering works on
every plan, so your casebook keeps learning in the meantime and the rule works the
moment you upgrade.
## Reading the stats
The four cards at the top of the Casebook page:
* **Awaiting review** — what's sitting in your review queue right now.
* **Handled for you** — the share of would-be reviews the casebook settled instead,
with the item count. Press it to open those items in the queue. While no channel
enforces, it reads **Casebook could have handled** — the reviews you'd have been
spared.
* **Casebook topics** — how many [topics](/casebook/topics) your cases are grouped
into.
* **Disagreements** — cases that rule opposite ways on similar content. Until someone
[resolves them](/casebook/managing#disagreements), the casebook applies neither
ruling to content like theirs.
## Where to go next
* [How the casebook rules](/casebook/rulings) — when it decides, when it deliberately
declines, and how to test it.
* [Topics](/casebook/topics) — how the casebook organizes what it remembers, and
rules that act on a whole topic.
* [Managing your casebook](/casebook/managing) — imports, disagreements, and making
the casebook forget a decision.
# How the casebook rules
Source: https://docs.moderationapi.com/casebook/rulings
How remembered decisions settle content, when the casebook deliberately declines, and how to test it
A casebook match always rules **allow** or **reject** — never review. Whatever it
can't settle goes to your [review inbox](/review/overview) as usual, so the casebook
only ever removes work, never hides it.
## Two ways to a ruling
* **Recognition.** The exact message has been ruled on before, so the same ruling
applies without a review.
* **Agreement among close cases.** The content is very similar to several existing
cases, and enough of them agree — "96% of its close cases agree, and that is enough
to rule."
Every ruling shows its evidence: the closest cases, each with a similarity
percentage, its ruling, who decided it, and its topic.
### Cases vote only on what they were flagged for
A case's ruling is scoped by the labels your policies flagged on it. A rejection
flagged for spam only counts as precedent for content flagged for spam — in a
lookup's evidence, a case that didn't participate reads *sat out — flags differ*, and
one that's too far off reads *not close enough to count*.
This is what makes corrected resubmissions safe: content close to rejected cases but
missing the flag that got them rejected goes to a person instead of inheriting the
rejection.
### Cautious in one direction
Overriding your policies takes more evidence than agreeing with them. The casebook is
quick to learn rules that are *stricter* than your policies — things your team
rejects that they let through — and deliberately slow to allow content they flagged
hard.
## When it declines to rule
Declining is a feature, not a failure. The casebook sends content to review when:
| Situation | What the casebook says |
| --------------------------- | -------------------------------------------------------------- |
| Nothing similar exists | "Your casebook has not seen anything like this." |
| Matches aren't close enough | "No cases close enough to apply." |
| Evidence is thin | "Close cases agree, but there are not enough of them yet." |
| Cases conflict | "Your cases disagree about content like this." |
| A new flag appeared | "Close to allowed cases, but flagged for something new." |
| The old reason is gone | "Close to rejected cases, but their reason no longer applies." |
When evidence is thin, the result shows how far along the casebook is — "58% of the
way to ruling on its own" — so you know a few more decisions will settle content like
this.
If the casebook can't answer within its time budget, moderation simply proceeds
without it. It can only ever add a ruling, never delay one.
## Test your casebook
**Look up** on the Casebook page opens a test dialog: paste any message, pick a
channel, and see how your casebook would rule — the verdict, what your policies
flagged, and the closest cases. Testing never teaches it anything.
When the lookup finds nothing, **Add this as a case** teaches the decision on the
spot.
## Precedent while you review
In the item detail view, a **Casebook** tab sits at the right edge. Pull it out and it
shows what your casebook makes of the item on screen — the same answer the lookup
gives, with each case one click from the item it was decided on. Leave it out and it
answers for every item you open. It reads and never writes: your queue decisions are
still what the casebook learns from.
## In the API response
When the casebook has a verdict for a request, the moderation response carries a
`casebook` object — the verdict, how close and how unanimous the supporting cases
were, and the topic they belong to. See
[checking the casebook](/content-moderation/acting-on-responses#check-the-casebook)
for the field reference and code examples.
Whether the verdict was *enforced* depends on the channel's Casebook lookup rule —
when it was, `recommendation.reason_codes` contains `casebook_match`, and the item
shows **Casebook lookup** as its outcome in the dashboard.
# Casebook topics
Source: https://docs.moderationapi.com/casebook/topics
Your precedent, organized into named themes you can browse, pin, and write rules against
Topics are the table of contents of your casebook. As cases accumulate, they're
grouped into plainly named themes — "Telegram spam", "Insults between players",
"Heated product feedback" — so you can see what kinds of content your team is ruling
on, and how.
Each topic shows its case count, its ruling tendency — *Always rejected*, *Always
allowed*, or a split like *42% rejected* — its keywords, and recent cases. **View all
cases** filters the ledger below to the topic.
A split tendency isn't a defect — it's often the insight. A team that rejects
cannabis sales while allowing other sales will see exactly that split, and can dive
into the topic to check the boundary is where they want it.
The last row is always **No topic yet**: cases still being committed to memory,
waiting for the next organizing pass, or one-offs too unlike anything else to file.
## Young casebooks reorganize, grown ones settle
Topics appear on their own after about ten cases, and the page tells you which of two
phases you're in:
* **While the casebook is small**, every organizing pass re-reads the whole of it and
re-authors the topics. Yesterday's names aren't binding — a topic that was really
just the first week's traffic gets renamed or dropped as the picture fills in.
* **As it grows**, topics stop moving and passes only add new ones, so the vocabulary
your team has been reading for weeks stays put.
Organizing runs in the background — the footer shows when the last pass ran, and
**Reorganize now** asks for a fresh one.
## Pinning
Pinning a topic is how you say "keep this". A pinned topic keeps its name and
survives every organizing pass; new cases can still be filed into it. You can also
**Rename topic** — renaming or pinning tells automation to leave the topic alone.
Writing a rule about a topic pins it automatically, so nothing your rules depend on
can be reshaped underneath you.
## Rules that act on a topic
Any [content rule](/content-moderation/rules) can be scoped to a casebook topic —
*if the content matches this topic, then take this action*. That lets you act on a
whole kind of content before enough precedent has accumulated for the casebook to
settle it case by case: send a borderline topic to review, or reject a scam topic
outright.
## The weekly digest
When your casebook identifies a new topic, you get a weekly summary email. Nothing is
sent in a week with no new topics, and you can turn it off in your notification
settings under **New casebook topics**.
# Understand and act on API responses
Source: https://docs.moderationapi.com/content-moderation/acting-on-responses
Learn what the API response contains and how to act on it.
The moderation API returns a structured response that helps you decide how to handle content. Here's an example response:
```json Example response theme={"theme":"nord"}
{
"content": {
"id": "message-123",
"masked": true,
"modified": "This is a test, my email is {{ email hidden }}"
},
"author": {
"id": "auth_abc123",
"external_id": "user-123",
"status": "enabled",
"block": null,
"trust_level": {
"level": 2,
"manual": false
}
},
"evaluation": {
"flagged": true,
"flag_probability": 0.87,
"severity_score": 0.3,
"unicode_spoofed": false
},
"recommendation": {
"action": "review",
"reason_codes": ["severity_review"]
},
"casebook": {
"verdict": "reject",
"similarity": 0.94,
"agreement": 0.92,
"confidence": 0.78,
"case_count": 6,
"topic": {
"id": "clx8k2p00000",
"label": "Competitor spam"
}
},
"policies": [
{
"id": "personal_information",
"type": "entity_matcher",
"probability": 0.95,
"flagged": true,
"flagged_fields": [],
"matches": [
{
"probability": 0.95,
"match": "test@example.com",
"span": [28, 44]
}
]
},
{
"id": "toxicity",
"type": "classifier",
"probability": 0.12,
"flagged": false,
"labels": [
{
"id": "toxic",
"probability": 0.12,
"flagged": false
}
]
}
],
"insights": [
{
"id": "sentiment",
"type": "insight",
"probability": 0.78,
"value": "neutral"
},
{
"id": "language",
"type": "insight",
"probability": 0.99,
"value": "en"
}
],
"meta": {
"status": "success",
"timestamp": 1735902168566,
"channel_key": "default",
"usage": 1,
"processing_time": "245ms"
}
}
```
How you act on the response is up to you:
* Block the content and return an error message to your user if it gets flagged
* Store it to your database and let a human review it using [review queues](/review/overview)
* Do something in between and only review content when the AI is not confident in its decision
***
## Use the recommendation
The easiest way to handle moderation responses is to use the `recommendation` object. It provides a clear action based on your channel configuration, severity scores, and author status.
```javascript theme={"theme":"nord"}
switch (response.recommendation.action) {
case "reject":
// Block the content, show an error to the user
throw new Error("Content not allowed");
case "review":
// Save the content but flag for manual review
await saveContent(content, { needsReview: true });
break;
case "allow":
// Content is approved, proceed normally
await saveContent(content);
break;
}
```
The `reason_codes` array tells you why a particular recommendation was made:
| Reason code | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| `rule_match` | A [content rule](/content-moderation/rules) matched — the action came from that rule |
| `rule_fallback` | No rule matched, the severity score triage fallback assigned the action |
| `rule_default` | No rule matched and triage is disabled — fell back to the channel default |
| `severity_reject` | Content severity score exceeded the rejection threshold |
| `severity_review` | Content severity score exceeded the review threshold |
| `author_block` | The author is blocked or suspended |
| `casebook_match` | Your [casebook](#check-the-casebook) recognised the content and applied your earlier ruling |
| `trusted_allow` | A rule allowed the content because the author's trust level is high |
| `client_override` | A [`clientAction`](/content-moderation/submit-content#clientaction) you submitted changed the recommendation |
| `dry_run` | Dry run mode is enabled (always returns `allow`) |
***
## Check if flagged
For simple use cases, you can check the `evaluation.flagged` field. This boolean indicates if any of your enabled policies detected something that triggered a flag.
```javascript theme={"theme":"nord"}
if (response.evaluation.flagged) {
// Content was flagged by at least one policy
}
```
### Severity score
The `severity_score` gives you more granular control. Higher scores indicate more severe violations:
```javascript theme={"theme":"nord"}
if (response.evaluation.flagged && response.evaluation.severity_score > 0.7) {
// High severity - reject immediately
} else if (response.evaluation.flagged) {
// Lower severity - send to review queue
}
```
We recommend using the `recommendation.action` field instead of implementing your own threshold logic. Configure thresholds in your channel settings for easier management.
***
## Check the casebook
Your casebook is the record of how your team has ruled on content in the past. When you
submit something close to a case it already holds, the response carries a `casebook`
object describing what it found.
```javascript theme={"theme":"nord"}
if (response.casebook) {
console.log(
`Casebook says ${response.casebook.verdict}, based on ${response.casebook.case_count} past cases`
);
}
```
| Field | Description |
| ------------ | ------------------------------------------------------------------------------------------- |
| `verdict` | The ruling your past decisions point to: `allow` or `reject` |
| `similarity` | How close the nearest backing case is, 0-1. `1` means identical content |
| `agreement` | How unanimous the backing cases are, 0-1 — ignores how many there are |
| `confidence` | How strongly the casebook holds the ruling, 0-1 — agreement scaled by evidence |
| `case_count` | How many of your past cases backed this ruling |
| `topic` | The topic the closest backing case is filed under, or `null` if it has not been grouped yet |
### Agreement vs. confidence
Both are 0-1 strength numbers, and they answer different questions:
**`agreement`** is unanimity, ignoring volume. Two cases that both say reject score the
same as fifty that do. It is always at least `0.8` when a verdict is returned — below
that the casebook reports a disagreement rather than picking a side — so treat it as a
description of how clean the consensus was, not as a threshold to re-apply yourself.
**`confidence`** is that agreement scaled by how much evidence backs it, so a handful of
close, recent cases outweighs a single distant one. Older cases count for less, halving
in weight roughly every 180 days. This is the number to use when you want a strength
condition, and it is the one available in [rules](/content-moderation/rules) as
`casebook.confidence`.
A high `agreement` with a low `confidence` means your cases were unanimous but there
were few of them, or they were old. That is the casebook telling you it has an opinion
it does not hold strongly.
### When `casebook` is null
The field is `null` whenever the casebook has nothing to say, which covers several
situations you do not need to tell apart:
* Nothing in your casebook was close enough to the content.
* The matching cases contradicted each other, so the casebook declined to pick a side.
* Casebook lookups are not switched on for the channel handling the request.
* Your plan does not include casebook lookups.
Always guard on it rather than reading through:
```javascript theme={"theme":"nord"}
// casebook may be null
if (response.casebook?.verdict === "reject") {
// ...
}
```
`casebook` reports what your casebook *found*. Whether it decided the outcome is a
separate question — check `recommendation`, where a higher-priority rule may have
settled the item first. When the casebook did decide, `recommendation.reason_codes`
contains `casebook_match`.
***
## Work with individual policies
The `policies` array contains results from each policy enabled in your channel, sorted by highest probability. Each policy includes:
| Field | Description |
| ---------------- | ---------------------------------------------------------------- |
| `id` | The policy identifier (e.g., `toxicity`, `personal_information`) |
| `type` | Either `classifier` or `entity_matcher` |
| `probability` | Model confidence level (0-1) |
| `flagged` | Whether this policy triggered based on your thresholds |
| `flagged_fields` | For object submissions, which fields triggered the flag |
### Classifier policies
Classifier policies (like `toxicity`, `spam`, `hate`) analyze content and return a probability score:
```javascript theme={"theme":"nord"}
const toxicityPolicy = response.policies.find(p => p.id === "toxicity");
if (toxicityPolicy?.flagged) {
console.log(`Toxicity detected with ${toxicityPolicy.probability * 100}% confidence`);
}
// Check specific labels within a classifier
if (toxicityPolicy?.labels) {
const severeLabel = toxicityPolicy.labels.find(l => l.id === "severe");
if (severeLabel?.flagged) {
// Handle severe toxicity differently
}
}
```
### Entity matcher policies
Entity matcher policies (like `personal_information`, `url`) detect and extract specific entities:
```javascript theme={"theme":"nord"}
const piiPolicy = response.policies.find(p => p.id === "personal_information");
if (piiPolicy?.matches?.length > 0) {
console.log("Found PII:");
piiPolicy.matches.forEach(match => {
console.log(` - "${match.match}" at position ${match.span[0]}-${match.span[1]}`);
});
}
```
***
## Handle masked content
If you have PII masking enabled, the API can automatically redact sensitive information. Check the `content` object:
```javascript theme={"theme":"nord"}
if (response.content.masked) {
// Use the modified content with masked values
const safeContent = response.content.modified;
await saveToDatabase(safeContent);
} else {
// No masking applied, use original content
await saveToDatabase(originalContent);
}
```
This is useful for:
* Anonymizing content before storing in your database
* Preventing users from seeing personal information
* Compliance with data protection regulations
***
## Check author status
If you're using [author management](/users/overview), the response includes author information:
```javascript theme={"theme":"nord"}
const { author } = response;
if (author?.status === "blocked") {
// Author is permanently blocked
throw new Error("Your account has been blocked");
}
if (author?.status === "suspended") {
const until = new Date(author.block.until);
throw new Error(`Your account is suspended until ${until.toLocaleDateString()}`);
}
// Optionally adjust behavior based on trust level
if (author?.trust_level.level >= 3) {
// Trusted author - maybe skip certain checks
}
```
***
## Use insights
The `insights` array provides additional analysis that doesn't affect flagging:
```javascript theme={"theme":"nord"}
const sentimentInsight = response.insights.find(i => i.id === "sentiment");
const languageInsight = response.insights.find(i => i.id === "language");
console.log(`Language: ${languageInsight?.value}`); // e.g., "en"
console.log(`Sentiment: ${sentimentInsight?.value}`); // "positive", "neutral", or "negative"
// Route negative sentiment to priority review
if (sentimentInsight?.value === "negative" && response.evaluation.flagged) {
await addToPriorityQueue(content);
}
```
***
## Detect unicode spoofing
Spammers sometimes use look-alike characters to bypass moderation (e.g., `mоney` with a Cyrillic "о" instead of Latin "o").
The API detects and normalizes these characters. Check the `unicode_spoofed` field:
```javascript theme={"theme":"nord"}
if (response.evaluation.unicode_spoofed) {
console.log("Content contains look-alike characters");
// The policies ran on normalized text for accurate detection
console.log(response.content.modified); // the cleaned-up message
}
```
The field is omitted when neither normalization nor the Unicode spoofing policy is enabled on the channel, since nothing analyzed the text.
To flag and route spoofed content rather than just observe it, enable the [Unicode spoofing policy](/policies/unicode-spoofing).
***
## Handle errors
Check `meta.status` for the overall request status:
```javascript theme={"theme":"nord"}
if (response.meta.status === "partial_success") {
// Some policies failed - check the errors array
response.errors?.forEach(error => {
console.warn(`Policy ${error.id} failed: ${error.message}`);
});
}
```
The `errors` array contains details about any policies that encountered issues during processing.
# Analyze audio
Source: https://docs.moderationapi.com/content-moderation/analyze-audio
How audio moderation works, supported formats, limits, and transcription quality settings.
## How it works
Audio files are automatically transcribed to text using speech recognition, then the transcript is analyzed by all enabled text-based policies. This means any policy that works on text (toxicity, hate, PII, wordlists, guidelines, etc.) also works on audio with zero additional configuration.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "audio",
url: "https://example.com/audio.mp3",
},
});
```
## Supported audio formats
Any format FFmpeg can decode is supported. All audio is internally converted to 16 kHz mono WAV before transcription.
| Format | Extensions |
| ------ | ---------------------- |
| MP3 | `.mp3` |
| WAV | `.wav` |
| AAC | `.aac`, `.m4a` |
| OGG | `.ogg`, `.oga` |
| Opus | `.opus` |
| FLAC | `.flac` |
| WebM | `.webm` |
| AMR | `.amr` |
| WMA | `.wma` |
| MP4 | `.mp4`, `.m4a`, `.mov` |
## Limits
| Constraint | Value |
| -------------------- | ------------------------- |
| Max file size | 50 MB |
| Max audio duration | 10 minutes |
| Processing timeout | 30 seconds |
| URL schemes | `http`, `https` only |
| Private/internal IPs | Blocked (SSRF protection) |
## Transcription quality
You can configure transcription quality per channel in the dashboard under **Content > Audio > Transcription quality**.
| Setting | Label | Use case | Relative speed |
| --------------- | -------- | ------------------------------------ | -------------- |
| SPEED (default) | Fast | Real-time moderation, high volume | Fastest |
| BALANCED | Balanced | General purpose, good accuracy | \~2x slower |
| ACCURACY | Accurate | Noisy audio, critical content review | \~3x slower |
## Usage and billing
Each audio moderation request costs **2 units**: 1 for transcription + 1 for policy analysis.
# Analyze images
Source: https://docs.moderationapi.com/content-moderation/analyze-images
Image models are enabled on an account basis. If you don't have access to image models, please contact our support team.
# Analyze videos
Source: https://docs.moderationapi.com/content-moderation/analyze-videos
How video moderation works, supported formats, limits, and frame sampling settings.
## How it works
Frames are sampled from the video at a configurable interval, then each frame is analyzed by all enabled image-based policies (NSFW, toxicity, etc.). If any sampled frame is flagged, the video is flagged. This means any policy that works on images also works on video with zero additional configuration.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "video",
url: "https://example.com/video.mp4",
},
});
```
## Supported video formats
| Format | Extensions |
| --------- | ---------- |
| MP4 | `.mp4` |
| QuickTime | `.mov` |
| M4V | `.m4v` |
| MPEG | `.mpeg` |
## Limits
| Constraint | Value |
| ------------------ | ---------- |
| Max file size | 100 MB |
| Max duration | 10 minutes |
| Processing timeout | 10 minutes |
Videos longer than 10 minutes are rejected with an error rather than partially analyzed. Split longer videos into segments of 10 minutes or less before submitting them.
## Frame sampling
You can configure how frames are sampled per channel in the dashboard under **Content > Video > Video sampling**.
| Setting | Default | Description |
| -------------------- | ------------------ | ---------------------------------------------------------------------------- |
| Capture interval | 1 second (min 0.5) | Time between sampled frames. |
| Maximum frames | 10 | Caps the number of frames analyzed per video. Use this to control max spend. |
| Spread frames evenly | On | Distributes frames across the video duration instead of from the start. |
## Usage and billing
Each analyzed frame costs **3 units**. Total usage for a video is the number of sampled frames × 3 — for example, a video sampled at 8 frames costs 24 units. Lower the **Maximum frames** setting to cap usage per video.
# Optimizing accuracy
Source: https://docs.moderationapi.com/content-moderation/optimizing-accuracy
The Moderation API is designed to work for a broad range of use cases, but you might need to make adjustments for optimal accuracy in your context.
## False positives and negatives
When you’re relying on a content moderation system, a key concept to understand is the trade-off between false positives and false negatives:
* **False positives** occur when a system flags content as violating policy when it is actually compliant.
* **False negatives** occur when a system fails to flag content that does violate policy.
It’s crucial to decide which type of error is more costly to your specific use case. Some use cases demand minimal tolerance for any potentially harmful content (prioritizing avoiding false negatives), while others might tolerate a small amount of non-harmful content being flagged incorrectly (i.e., false positives).
Balancing these is an ongoing process. Consider the following strategies:
* Adjust thresholds (covered below) to tightly align with your standards and risk appetite.
* Supplement automated checks with human moderation for borderline cases to reduce both types of errors.
## Adjusting flagging thresholds
The Moderation API allows configuration for how strictly it flags content. By experimenting with threshold scores, you can tweak the sensitivity:
1. **Lowering the threshold:** This will reduce false negatives (because more content is flagged), but potentially increase false positives.
2. **Raising the threshold:** This will reduce false positives (because the system flags fewer pieces of content), but potentially increase false negatives.
It’s helpful to analyze your actual data for patterns. If you notice consistent misclassifications in content that truly violates your policies, lower the threshold. Conversely, if you see a flood of harmless content getting flagged, consider raising it. In production, you might set different thresholds per category (e.g., hateful vs. spam content) based on your tolerance.
Read more about [how to adjust thresholds](/content-moderation/thresholds).
## Adding or removing models
Your workflow or platform could be improved by adding specialized models or removing unnecessary ones:
* **Adding specialized models**: If your use case deals with a specific domain (e.g., medical or legal), you might consider training or deploying a model tuned for that domain. This model could work in tandem with the general Moderation API to reduce domain-specific false flags.
* **Removing unneeded models**: If your current pipeline includes multiple checks from overlapping or redundant models, rationalizing them may reduce complexity and potential conflicts in results. It can also streamline moderation decisions.
## Writing custom guidelines
When pre-built policies don't quite fit your platform, [Guidelines](/policies/guidelines) let you describe what isn't allowed in plain English. Each guideline is evaluated by an LLM with your project context in scope, so the rule is interpreted against what your platform is and who uses it.
Guidelines work well for:
* Platform-specific rules that aren't covered by the standard categories.
* Cases where intent matters more than the literal words used.
* Iterating quickly — you can change a guideline by editing one sentence rather than retraining a model.
## Letting reviews set precedent
Human review is most valuable when a decision counts more than once. The
[casebook](/casebook/overview) remembers every allow and reject your team makes in
review and applies the same ruling to similar content, so a correction made once
doesn't need making again. Both error types benefit: false positives your team keeps
re-approving and false negatives they keep rejecting become precedent instead of
repeat work.
## Enabling context awareness
Many moderation challenges arise when the system doesn’t understand the broader context behind the text:
* Certain terms might be acceptable in an educational or reclaiming context (e.g., quoting a slur to explain its original meaning).
* Cultural or community-specific language usage might not translate well with a general-purpose model.
Some ways to incorporate context:
* Provide additional metadata or preceding conversation snippets along with your text, to give the Moderation API or other classifiers a better understanding of what’s being said.
* Enable Context Awareness in your project settings and include `authorId` and `conversationId` in your requests so the system can reference previous messages. See [Submitting content to Moderation API](/content-moderation/submit-content) for more details.
## Training custom models
If you find that standard models are not meeting your performance requirements, consider building a custom fine-tuned model. This can help if:
* You have a substantial dataset specific to your industry or type of content.
* You need higher precision for borderline or ambiguous cases.
* You want to reduce reliance on manual moderation for specialized content.
With careful curation of training data and setting well-defined moderation policies, a custom model can further reduce both false positives and false negatives, fine-tuning decisions for your exact use case.
## Get help from our team
If you need help deciding how to set up moderation, fine-tuning or advanced configurations, reach out to our support teams. We can help you:
* Determine the right thresholds for your workflow.
* Explore sample code to integrate moderation into your application.
* Identify potential pitfalls with domain-specific moderation.
Working together, we can ensure that your content moderation solution is well-aligned with your application’s requirements—minimizing harm, maximizing safety, and providing users with the best possible experience.
# Automated content moderation
Source: https://docs.moderationapi.com/content-moderation/overview
Learn to set up projects and submit content for moderation
## Analyzing content
After you've created a project, you can submit content for moderation.
How to analyze content.
## Optimization
As you analyze content, you can use the results to improve the accuracy of the moderation and stay up to date with the latest content.
How to improve the accuracy of the moderation.
# Real-time voice
Source: https://docs.moderationapi.com/content-moderation/real-time-voice
Moderate live voice and call audio in real time, with per-utterance verdicts grouped into conversations.
Real-time voice moderation is available to customers on custom plans. If you're interested in using it, please [reach out here](https://moderationapi.com/sales).
## How it works
Voice moderation analyzes live voice and call audio as it happens. You open a streaming connection and send call audio; the speech is transcribed and each finalized utterance is moderated by your enabled text policies—toxicity, hate, PII, wordlists, guidelines, and the rest—with no extra configuration. You receive a moderation result for every utterance as it's spoken.
Unlike [audio file moderation](/content-moderation/analyze-audio), which analyzes a complete recording after the fact, voice moderation works on a live stream and returns a verdict for each utterance during the call.
## Conversations
A voice call is a **conversation**: a single live session with a start and an end, where every utterance belongs to the same thread. This lets you review an entire call as one unit instead of a series of disconnected messages.
* **Bring your own id.** Supply a `conversationId` to link the call to a record in your own system. If you don't, one is generated for you and returned when the session starts—every utterance in the call shares it.
* **Filter by type.** Voice utterances are tagged with the `voice` content type, so you can separate them from messages, posts, and other content.
Real-time voice is in early access and the streaming interface may still
change. Coordinate with us before building a production integration so we can
confirm the current contract and your account's limits.
## Connecting
Open a WebSocket connection to the streaming endpoint, authenticating with your API key on the upgrade request and requesting the `moderationapi.v1` subprotocol.
```
wss://voice.moderationapi.com/v1/stream
Authorization: Bearer
Sec-WebSocket-Protocol: moderationapi.v1
```
A missing or malformed key closes the connection with code `4401`.
### Start the session
Send a `start` frame as the first message. It declares the conversation, the audio format, and the tracks you'll stream (for example a caller and an agent), each with an optional author id.
```json theme={"theme":"nord"}
{
"event": "start",
"conversationId": "your-call-id",
"channel": "your-channel-key",
"mediaFormat": { "encoding": "audio/x-mulaw", "sampleRate": 8000 },
"tracks": [
{ "name": "inbound", "authorId": "caller-123" },
{ "name": "outbound", "authorId": "agent-456" }
],
"emitPartials": false,
"metadata": { "crmTicket": "T-9912", "region": "eu" }
}
```
* `conversationId` — optional. Omit it to have one generated and returned in `session.started`.
* `channel` — optional. Selects which channel's policy configuration applies.
* `tracks` — stream **one or both** tracks. Send both `inbound` and `outbound` to moderate the full call with each side attributed to its own author, or just one track (for example only `inbound`) if that's all you have access to. Audio for any track you don't declare is ignored.
* `mediaFormat.encoding` — `audio/x-mulaw` (`PCMU`), `audio/x-alaw` (`PCMA`), linear PCM (`audio/l16`, `linear16`), or common encoded containers (`wav`, `mp3`, `ogg`, `flac`). `sampleRate` may be 8000–48000 Hz. Audio is passed through without resampling. The spoken language is detected automatically.
* `emitPartials` — optional. Set `true` to also receive interim, non-final transcripts.
* `metadata` — optional, arbitrary JSON attached to the conversation. Put anything you want to associate with the call here (your own ids, tags, context); it's stored on the conversation and not interpreted by moderation.
The server replies with `session.started`:
```json theme={"theme":"nord"}
{ "v": 1, "event": "session.started", "conversationId": "your-call-id", "sessionId": "…", "tracks": ["inbound", "outbound"] }
```
### Stream audio
Send `media` frames as audio arrives, one per track, with the audio chunk base64-encoded in `payload`:
```json theme={"theme":"nord"}
{ "event": "media", "media": { "track": "inbound", "payload": "" } }
```
### End the session
Send a `stop` frame to end the call gracefully (or simply disconnect). The server drains any in-flight utterances, emits `session.ended`, and closes.
```json theme={"theme":"nord"}
{ "event": "stop" }
```
### Using Twilio or another telephony provider
Telephony providers like Twilio stream call audio but can't consume the moderation verdicts the gateway streams back, so they don't connect to the gateway directly. Instead, run a thin bridge in your own backend:
1. Accept the provider's media stream (for Twilio, its `connected` / `start` / `media` messages).
2. Open this WebSocket and map those onto the `start` and `media` frames above—pass your call id as `conversationId` and the caller/agent identifiers as each track's `authorId`.
3. Relay the `utterance.final` verdicts back to your application to act on them.
## Events you receive
Every outbound message carries `"v": 1` and an `event` field.
| Event | When |
| ------------------- | ------------------------------------------------------------ |
| `session.started` | After your `start` frame is accepted. |
| `utterance.partial` | Interim transcript (only if `emitPartials` was `true`). |
| `utterance.final` | A finalized utterance, with its moderation result. |
| `warning` | Non-fatal condition (e.g. a transient transcription hiccup). |
| `session.error` | Fatal error; the connection closes. |
| `session.ended` | The call ended; includes summary stats. |
The key event is `utterance.final`—the transcribed text plus the standard moderation result (`evaluation`, `recommendation`, and `policies`), in the same shape as every other moderation response:
```json theme={"theme":"nord"}
{
"v": 1,
"event": "utterance.final",
"conversationId": "your-call-id",
"contentId": "…",
"track": "inbound",
"authorId": "caller-123",
"text": "transcribed speech for this utterance",
"startMs": 0,
"endMs": 2000,
"sttConfidence": 0.95,
"evaluation": { "flagged": false },
"recommendation": { "action": "allow" },
"policies": []
}
```
Use the `recommendation.action` (`allow`, `review`, or `reject`) to decide what to do—see [Acting on responses](/content-moderation/acting-on-responses).
When the call ends you receive `session.ended` with a summary:
```json theme={"theme":"nord"}
{
"v": 1,
"event": "session.ended",
"conversationId": "your-call-id",
"sessionId": "…",
"stats": { "durationMs": 125000, "utterances": 42, "actions": { "allow": 39, "review": 2, "reject": 1 } }
}
```
### Close codes
| Code | Meaning |
| ------ | -------------------------------------- |
| `1000` | Normal close |
| `1011` | Server error |
| `4400` | Bad request (e.g. a malformed `start`) |
| `4401` | Authentication failed |
| `4403` | Not authorized for voice |
| `4429` | Concurrency limit reached—retry later |
## Example
A minimal Node.js client that opens a session, streams audio from your telephony source, and acts on each verdict:
```javascript theme={"theme":"nord"}
import WebSocket from "ws";
const ws = new WebSocket("wss://voice.moderationapi.com/v1/stream", "moderationapi.v1", {
headers: { Authorization: `Bearer ${process.env.MODERATION_API_KEY}` },
});
ws.on("open", () => {
ws.send(
JSON.stringify({
event: "start",
conversationId: "call-abc-123",
channel: "support-calls",
mediaFormat: { encoding: "audio/x-mulaw", sampleRate: 8000 },
tracks: [{ name: "inbound", authorId: "caller-123" }],
emitPartials: false,
metadata: { crmTicket: "T-9912" },
}),
);
});
// Forward audio as it arrives from your telephony source (base64-encoded chunks).
function sendAudio(base64Chunk) {
ws.send(JSON.stringify({ event: "media", media: { track: "inbound", payload: base64Chunk } }));
}
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
switch (msg.event) {
case "session.started":
console.log("session started:", msg.conversationId);
break;
case "utterance.final":
console.log(`[${msg.track}] ${msg.text} -> ${msg.recommendation.action}`);
if (msg.recommendation.action === "reject") {
// act on it — flag the call, alert an agent, etc.
}
break;
case "session.ended":
console.log("session ended:", msg.stats);
break;
}
});
// When the call ends, close the session gracefully.
function endCall() {
ws.send(JSON.stringify({ event: "stop" }));
}
```
## Limits
| Constraint | Value |
| ----------------- | ---------------------------------------------------------------------------- |
| Language | Detected automatically |
| Mode | Observe and report—verdicts are returned, the live call is not interrupted |
| Max call duration | 1 hour |
| Concurrent calls | Per-account limit; [contact us](https://moderationapi.com/sales) to raise it |
# Content rules
Source: https://docs.moderationapi.com/content-moderation/rules
Set up rules that decide whether content is allowed, sent to review, or rejected based on policy flags, severity, author trust, and language.
Each content rule reads as *if these conditions are true, then take this action*. Rules run top-to-bottom for every request, and the first match determines the `recommendation.action` returned by the API.
They give you one place to express the moderation logic for a channel.
## Where to find it
Open your project, pick a channel, and go to **Rules**. You'll see your rules listed in evaluation order, with the **Severity score triage** fallback at the bottom.
### Rules you start with
Every new channel is created with two author rules already enabled:
| Order | Rule | Condition | Action |
| ----- | --------------------- | --------------------------------- | ------ |
| 1 | Allow trusted authors | Trust Level is at least `Trusted` | Allow |
| 2 | Block banned authors | Status is not `Enabled` | Reject |
Both are ordinary rules — edit, reorder, disable, or delete them like any other. Everything below them falls through to severity score triage until you add more.
Note the order: because the first match wins, **Allow trusted authors** takes precedence over **Block banned authors**. If you [manually promote authors](/users/trust-levels#allowlist-a-specific-author) to `Trusted`, blocking one of them won't reject their content until you either move the block rule to the top or clear the trust level override.
Rules decide the **recommendation**, not whether content is analyzed. Every
request runs through the channel's policies first, so a matching Allow rule
still returns the full policy scores and `flagged` still reflects what the
analysis found. What changes is `recommendation.action`.
## Common use cases
### Auto-approve established users
Return `allow` for authors who've earned a track record on your platform, without waiting on the severity thresholds.
* **If** Trust Level is at least `Regular`
* **Then** Allow
`Regular` is reached automatically — 90+ days on your platform, 50+ clean items, and a violation rate at or below your threshold — so this covers the authors who proved themselves through their own behavior. Use `Member` to extend it to newer accounts, and see [trust levels](/users/trust-levels) for what each tier requires.
The rule shipped with each channel is stricter, matching only `Trusted`. Lowering it to `Regular` is the usual first adjustment once you trust the automatic tiers. `Trusted` is manual-only, so leave the threshold there if you want the rule to apply to nobody but the authors you [promote by hand](/users/trust-levels#allowlist-a-specific-author).
### Block banned users immediately
Reject anything from authors you've already disabled.
* **If** Status is not `Enabled`
* **Then** Reject
Move this rule **above** your trust level Allow rule if you use manual trust
level overrides. In the default order the Allow rule comes first, and since
the first match wins, a blocked author who was manually promoted to `Trusted`
still gets `allow`. Automatic trust levels don't have this problem — an author
whose content gets flagged drops out of the upper tiers on their own.
### Always reject the worst categories
If a category is zero-tolerance for your platform, send it straight to reject regardless of the severity score.
* **If** Illicit is `Flagged`
* **Then** Reject
### Catch risky links from new accounts
Spam and phishing often arrive as a fresh account dropping a link. Combining trust level with URL Risk lets you hold suspect new accounts without slowing down established users.
* **If** Trust Level is at most `New`
* **And** URL Risk is `Flagged`
* **Then** Review
### Tighten moderation for new accounts
Trigger review when a new account hits any flag, even if the severity score wouldn't normally cross your threshold.
* **If** Trust Level is at most `New`
* **And** Any Policy Flagged is `true`
* **Then** Review
### Route by language
Send anything you can't review in-house to a separate action.
* **If** Language is not `English`
* **Then** Review
***
## How rules work
### Conditions
Each rule has one or more conditions. Conditions inside a rule are joined with **AND**: every condition must be true for the rule to match.
A condition has three parts:
* **Field**: the signal you're matching on (Trust Level, Toxicity Score, Sentiment, ...)
* **Operator**: how to compare it (`is`, `is not`, `at least`, `is one of`, ...)
* **Value**: what you're comparing it to
Add more conditions with **+ Add condition** to make the rule narrower.
### Available signals
| Group | Field | Examples |
| -------- | ------------------ | ------------------------------------------------ |
| Author | Trust Level | Untrusted, New, Basic, Member, Regular, Trusted |
| Author | Status | Enabled, Blocked, Temporarily Blocked |
| Severity | Severity Score | A number between `0` and `1` |
| Insights | Sentiment | Positive, Neutral, Negative |
| Insights | Language | English, Spanish, French, ... |
| Policies | Any Policy Flagged | `true` / `false` |
| Policies | `` Flagged | `true` / `false`, per enabled policy |
| Policies | `` Score | A number between `0` and `1`, per enabled policy |
Policy fields appear automatically based on which policies are enabled in the channel. If you disable a policy that a rule still references, the rule is highlighted so you can update or remove it.
### Order matters
Rules run top-to-bottom and the **first** rule that matches wins. Drag rules by the handle on the left to reorder.
A typical ordering:
1. **Reject** rules for blocked users, so nothing below can approve them
2. **Allow** rules for trusted users, to short-circuit the rest of the list
3. **Reject** rules for zero-tolerance categories
4. **Review** rules for borderline cases
5. Severity score triage fallback at the bottom
Put the narrowest, most certain rules at the top. Anything that should hold regardless of the other signals — a blocked author, a zero-tolerance category — belongs above the broad Allow rules.
### Actions
Each rule resolves to one of three actions, returned as `recommendation.action`:
| Action | Use when |
| -------- | -------------------------------------------------- |
| `allow` | Content is fine, publish it |
| `review` | Hold or send to a [review queue](/review/overview) |
| `reject` | Block it |
### Severity score fallback
If no rule matches, the channel falls back to **Severity score triage**. This is a built-in step at the bottom of the list that assigns Allow, Review, or Reject based on the severity score thresholds set just below the rules. Click the row to see the current zones.
You can disable triage if you'd rather have non-matching content default to Allow without any severity check. See [Thresholds](/content-moderation/thresholds#severity-score-triage) for how to tune the cutoffs and use the calibration helper.
### Casebook lookup
Channels also carry a **Casebook lookup** row in the cascade — it applies the ruling your team gave to similar past content, and abstains when precedent is weak or conflicting. It's draggable and has its own switch like any rule; by default it runs just above the severity fallback. See [Casebook](/casebook/overview).
***
## Simulate before saving
Click **Simulate** to run your draft rules against recent moderation history for this channel. The result shows how the action mix shifts:
* How many recent items would now be Allowed, Reviewed, or Rejected
* The delta versus what actually happened
* The sample size and time range used
Use this to catch over-aggressive rules before they hit production. Simulation reads your unsaved changes, so you can iterate on the rule set safely.
***
## What you see in the API response
When a rule matches, the moderation response reflects it:
```json theme={"theme":"nord"}
{
"recommendation": {
"action": "review",
"reason_codes": ["rule_match"]
}
}
```
Reason codes you may see when rules are configured:
| Reason code | Meaning |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| `rule_match` | A configured rule matched, and the action came from that rule |
| `rule_fallback` | The severity score triage fallback matched |
| `rule_default` | No rule matched and triage is disabled, so the channel default was used |
| `client_override` | A [`clientAction`](/content-moderation/submit-content#clientaction) on the request changed the action after rules ran |
Read more about acting on the response in [Understanding API responses](/content-moderation/acting-on-responses).
If you submit a [`clientAction`](/content-moderation/submit-content#clientaction) with a request — your own recommendation from a blocklist or external tool — it's applied **after** your rules and the severity fallback. Depending on its `behavior`, it can escalate or override the action they produced.
***
## Tips
* **Start broad and tighten over time.** The two default author rules plus the severity fallback get most channels 80% of the way there. Add narrower rules as you spot patterns in the queue.
* **Don't be afraid to lean on severity score triage.** It does a good job out of the box: blocking the obvious stuff, sending borderline items to review, and letting clean content through. Rules are best for the cases triage can't express on its own (author trust, specific categories, language routing).
* **Give rules a real name.** They show up in dashboards and audit logs, so "Reject high toxicity" is more useful than "Rule 4". The pencil icon on an expanded rule lets you rename it.
* **Run Simulate after every change.** A condition that looks safe on paper can quietly shift hundreds of items between Review and Reject, and Simulate is the fastest way to spot it.
* **Toggle off instead of deleting.** When you're not sure if a rule is still pulling its weight, disable it. You keep the configuration around and can flip it back on if the data says otherwise.
# Submitting content to Moderation API
Source: https://docs.moderationapi.com/content-moderation/submit-content
How to submit content for moderation and which content types you can analyze with Moderation API.
To analyze content, send a POST request to the `/moderate` endpoint. The API accepts different content types (text, image, object, video, audio) and returns results immediately.
***
```typescript Node.js example theme={"theme":"nord"}
import ModerationAPI from "@moderation-api/sdk";
// Configure with environment variable MODAPI_SECRET_KEY
const moderationApi = new ModerationAPI();
// Text moderation
const textResult = await moderationApi.content.submit({
content: {
type: "text",
text: "Hello world!",
},
// Optional content data
contentId: "text-1",
authorId: "user-123",
conversationId: "room-456",
metadata: {
customField: "value",
},
});
// Use the API's recommendation
if (textResult.recommendation.action === "reject") {
// Block the content
} else if (textResult.recommendation.action === "review") {
// Send to moderation queue
} else {
// Content approved - add to database
}
// Image moderation
const imageResult = await moderationApi.content.submit({
content: {
type: "image",
url: "https://example.com/image.jpg",
},
// Optional content data
contentId: "image-1",
authorId: "user-123",
metadata: {
customField: "value",
},
});
// Simple flagged check
if (imageResult.evaluation.flagged) {
// Return error to user etc.
} else {
// Add to database etc.
}
```
```javascript Fetch example theme={"theme":"nord"}
const response = await fetch("https://api.moderationapi.com/v1/moderate", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
content: {
type: "text",
text: "Hello world!",
},
// Optional content data
contentId: "text-1",
authorId: "user-123",
conversationId: "room-456",
metadata: {
customField: "value",
},
}),
});
const result = await response.json();
if (result.evaluation.flagged) {
// Return error to user etc.
} else {
// Add to database etc.
}
```
***
## Content metadata
You can add metadata to the content you send for moderation. Some fields are used by the moderation pipeline to improve accuracy, while others enhance the dashboard experience.
### contentId
Specify a `contentId` to associate the request with specific content. This is typically the content's unique identifier from your database.
If you don't specify a `contentId`, the API generates a random ID for the content.
When you include a `contentId`, submitting the same ID again updates the existing content. This is useful when using [review queues](/review/overview) - you can update content in the queue without creating duplicate items.
The `contentId` can also be used to execute actions in the review queue programmatically. For example, you can allow users to report content on your platform and then add it to a review queue.
### conversationId
Use `conversationId` to group related content together, such as messages in a chatroom or comments on a post.
If you're using [review queues](/review/overview), the `conversationId` can filter the queue to show content from a specific conversation.
Enable [context awareness](#context-awareness) to improve moderation accuracy using the `conversationId`.
### authorId
Use `authorId` to identify the user who created the content.
This enables user-level moderation in [review queues](/review/overview) and allows filtering by specific users.
Enable [context awareness](#context-awareness) to improve moderation accuracy using the `authorId`.
### metaType
Use `metaType` to specify what kind of content you're moderating. This helps the API apply appropriate analysis:
| Value | Use case |
| --------- | ------------------------------- |
| `message` | Chat messages, direct messages |
| `post` | Forum posts, social media posts |
| `comment` | Comments on posts or articles |
| `review` | Product or service reviews |
| `profile` | User profile information |
| `product` | Product listings |
| `event` | Event descriptions |
| `other` | Any other content type |
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "text",
text: "Great product, highly recommend!",
},
metaType: "review",
authorId: "user-123",
});
```
### channel
Use `channel` to route content to a specific channel configuration. If not provided, the project's default channel is used.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "text",
text: "Hello world!",
},
channel: "high-risk-content",
});
```
### metadata
Use `metadata` to attach any additional information to the request. This object can contain custom key-value pairs.
Metadata is displayed in [review queues](/review/overview) and included in webhooks.
If you add a link in metadata, it will be clickable from the review queue. This is useful for linking back to the original content in your application.
***
## Submit your own moderation recommendation
If you already run your own flagging — a blocklist, a third-party tool, or your own logic — you can submit that recommendation alongside the content and have it combined with the API's own analysis.
Without this, your own flags live outside Moderation API, so content you've flagged but our analysis considered clean never shows up where your moderators work.
### clientAction
Use `clientAction` to submit your recommendation. It's not a log of an action you took — it's an input the API weighs alongside its own analysis to produce the final recommendation.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "text",
text: "Check out my site: example.com",
},
contentId: "post-789",
authorId: "user-123",
clientAction: {
action: "review",
behavior: "escalate",
source: "ip-blocklist",
reason: "Author IP is on our internal blocklist",
},
});
// recommendation.action is now at least "review", and
// recommendation.reason_codes includes "client_override"
```
The `clientAction` object accepts:
| Field | Required | Description |
| ---------- | -------- | ------------------------------------------------------------------------------------- |
| `action` | Yes | Your recommendation: `allow`, `review`, or `reject` |
| `behavior` | No | How your recommendation combines with ours (see below). Defaults to `escalate` |
| `source` | No | Where your recommendation came from, e.g. `"ip-blocklist"`. Shown in the review queue |
| `reason` | No | A human-readable explanation, shown in the review queue |
**Behavior** controls how your recommendation combines with the API's own recommendation:
| Behavior | What happens |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `escalate` (default) | Your recommendation is applied only when it's *stricter* than ours — `reject` is stricter than `review`, which is stricter than `allow`. This lets you push content into review or reject, but never loosens the API's own decision. |
| `override` | Your recommendation replaces the API's recommendation entirely, even if that means loosening it. |
When your recommendation changes the outcome, the response includes the `client_override` [reason code](/content-moderation/acting-on-responses#use-the-recommendation).
`clientAction` sets `recommendation.action` and adds the `client_override` reason code, but it does **not** change `evaluation.flagged` — that always reflects the API's own analysis.
Content you escalate to `review` this way still enters the [inbox](/review/overview) — a review recommendation is exactly what the inbox collects. If you work from a [content view](/review/content-views) filtered by flag status, make sure its filter includes these items, since `evaluation.flagged` can still be `false`.
***
## Context awareness
Enable `Context awareness` in your channel settings, then include `authorId` and/or `conversationId` in API requests. This allows models to analyze previous messages for improved accuracy.
When context awareness is enabled, models analyze the current message alongside recent conversation history. The API retrieves previous messages with the same `conversationId` or `authorId` and provides them to the model sequentially, allowing it to understand the full context before making a decision.
LLM-based policies can use the `conversationId` to see previous messages in the same conversation, and `authorId` to see previous messages from the same author.
This can prevent unwanted content spread across multiple messages:
```
msg 1 -> f
msg 2 -> u
msg 3 -> c
msg 4 -> k [FLAGGED with context awareness]
```
It also helps understand messages in the context of a conversation:
```
user 1 -> What's the worst thing you know?
user 2 -> European people [FLAGGED with context awareness]
```
***
## Content types
The `/moderate` endpoint accepts different content types through the `content` object:
Analyze text content
Analyze image content
Analyze mixed content types
Analyze video content
Analyze audio content
### Text
Text moderation is the most common type. Use it for:
* Chat messages
* Forum posts
* Comments
* Reviews
* Product descriptions
* Profile bios
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "text",
text: "Hello world!",
},
});
```
If you're analyzing chat messages or thread-based content, enable [context awareness](#context-awareness) for better accuracy.
### Image
Image moderation analyzes visual content to detect inappropriate or harmful images, including nudity, violence, or other objectionable content.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "image",
url: "https://example.com/image.jpg",
},
});
```
### Object
Object moderation analyzes multiple fields at once, useful for moderating entire entities like user profiles or product listings.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "object",
data: {
title: { type: "text", text: "Product name" },
description: { type: "text", text: "Product description" },
image: { type: "image", url: "https://example.com/product.jpg" },
},
},
metaType: "product",
});
```
The response includes `flagged_fields` in each policy result, showing which specific fields triggered the flag.
### Audio
Audio files are automatically transcribed to text, then analyzed by all enabled text-based policies. Useful for podcasts, voice messages, or other audio content. See [Analyze audio](/content-moderation/analyze-audio) for supported formats, limits, and transcription quality settings.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "audio",
url: "https://example.com/audio.mp3",
},
});
```
### Video
Frames are sampled from the video and analyzed by all enabled image-based policies, detecting inappropriate scenes such as nudity or other objectionable content. See [Analyze videos](/content-moderation/analyze-videos) for supported formats, limits, and frame sampling settings.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "video",
url: "https://example.com/video.mp4",
},
});
```
***
## Opt out of content store
Set `doNotStore` to `true` to prevent the content from being stored. The content will still be analyzed but won't appear in the dashboard or review queues.
```javascript theme={"theme":"nord"}
const result = await moderationApi.content.submit({
content: {
type: "text",
text: "Hello world!",
},
doNotStore: true,
});
```
Setting `doNotStore` to `true` will make parts of the moderation dashboard less useful, as content won't be available for review or analysis.
Do not disable content storage if you want to train or optimize models based on your data.
# Thresholds
Source: https://docs.moderationapi.com/content-moderation/thresholds
Tune what counts as flagged for each policy and what action the channel takes by default. These are your two main dials for the false positive / false negative trade-off.
A channel has two layers of thresholds:
1. **Per-policy thresholds** decide *what counts as flagged* for each policy.
2. **Severity score triage** decides *what action to take* when content is flagged and no [content rule](/content-moderation/rules) matches.
Both live on the channel and can be tuned independently. Most teams start with the defaults, watch a few hundred items go through, then nudge the dials based on what they see in the review queue.
***
## Severity score triage
When a moderation request doesn't match any content rule, the channel falls back to **severity score triage**. Triage takes the request's `severity_score` (a 0–1 number summarizing how problematic the content is) and assigns Allow, Review, or Reject based on two thresholds.
### Where to find it
Open your project, go to **Rules**, and scroll to **Severity score triage**. If the project has more than one channel, pick the channel from the switcher at the top of the page.
### The two thresholds
| Threshold | Default | What it does |
| ---------------- | ------- | ------------------------------- |
| Review threshold | `50%` | Below this, content is allowed |
| Block threshold | `90%` | Above this, content is rejected |
Anything between the two is sent to **review**.
The card shows a histogram of recent severity scores in this channel, color-coded by which zone each item falls into at the current settings. You can see how a slider change would shift your review queue load and reject rate before you save.
### Presets
Use the preset dropdown for a starting point:
| Preset | Review / Block | When to use |
| ----------------- | -------------- | ------------------------------------------------------------ |
| Strict | `40% / 70%` | Conservative platforms that want to minimize false negatives |
| Balanced | `50% / 90%` | Default for most general-purpose moderation |
| Forgiving | `70% / 95%` | Communities tolerant of edge cases |
| Skip reviewing | `75% / 75%` | Auto allow or reject, no review queue |
| Always review | `50% / 100%` | Send borderline content to review, never auto-reject |
| Review everything | `0% / 100%` | Manual moderation, every flagged item goes to a human |
| Allow everything | `100% / 100%` | Observe-only mode, log scores but never block |
The [casebook](/casebook/overview) runs alongside triage and can overrule it where
your team's past decisions disagree with it. The queue's **Casebook overruled**
filter shows where — a steady stream of overrulings on one side is a sign a
threshold sits in the wrong place.
### Disable triage
Toggle the switch on the **Severity score triage** row in the rules list to turn the fallback off completely. With triage disabled, anything that doesn't match a rule is allowed.
This is useful when:
* You've moved all your decision logic into [content rules](/content-moderation/rules) and want a single source of truth.
* You're observing the system in shadow mode and want every request to pass through unless a rule says otherwise.
### Calibration helper
The **Calibrate** button on the severity triage card pulls items your team has already resolved in the [review queue](/review/overview) and recommends thresholds that maximize agreement between your pipeline and your moderators' decisions.
The flow is:
1. Pull a sample of resolved items (allowed and rejected) from the queue.
2. Compare the severity scores of allowed vs. rejected items to find the natural review zone.
3. Preview the suggested thresholds against your data before applying.
You'll get the most useful recommendations after your team has resolved a few hundred items in the queue.
***
## Per-policy thresholds
Each classifier policy returns a `probability` between `0` and `1` for every request. The **detection threshold** is the cutoff at which that probability flips the policy's `flagged` flag from `false` to `true`.
A flagged policy contributes to the overall severity score, can be referenced directly in [content rules](/content-moderation/rules#available-signals) (e.g. *Toxicity is Flagged*), and surfaces in the review queue.
### Where to find it
Open your project, go to **Policies**, pick the category (Toxicity, NSFW, Illicit, ...), expand the policy, and select the **Threshold** tab. If the project has more than one channel, pick the channel from the switcher at the top of the page.
### How to tune it
The slider shows a histogram of recent confidence scores for that policy. As you move the slider:
* **Bars to the left** of the slider are below threshold → not flagged
* **Bars to the right** are at or above threshold → flagged
Two KPI cards show how many recent items would have flipped sides. Use this to size the impact before saving.
A general rule:
* **Lower the threshold** if you're seeing false negatives: content you'd want flagged is slipping through.
* **Raise the threshold** if you're seeing false positives: borderline content is being flagged unnecessarily.
Click **Reset** to revert to the system default for that policy.
### Flag vs. shadow flag
Each policy also has a **Flagging** tab with two options:
| Option | Behavior |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Flag content when detected | Standard. Crossing the threshold sets `flagged: true`, contributes to severity, and the policy can be referenced in rules |
| Shadow flag when detected | The policy still scores content but doesn't mark it flagged. Items still appear in the queue under the *shadow flagged* filter |
Shadow flagging is useful for testing a new policy in production without changing what your users experience. You can watch how it would score real traffic, validate it against your moderators' decisions, then switch it on for real.
***
## How the two layers interact
For a single moderation request, here's the order things happen in:
Every enabled policy returns a `probability`. Per-policy thresholds decide which policies set `flagged: true`.
Policies that scored contribute to the overall `severity_score`. The more
severe the violation, the higher the score.
Your [rules](/content-moderation/rules) run top-to-bottom. The first match
decides the recommended action.
If no rule matches, the severity score triage thresholds decide the action: Allow, Review, or Reject.
So per-policy thresholds control *what gets flagged* and shape the severity score. Severity triage thresholds control *what action is taken* when no rule explicitly handles the case.
If you want a category to never auto-reject regardless of score, write a [rule](/content-moderation/rules) that catches it. Rules run before triage, so a matching rule wins.
***
## Tips
* **Change one dial at a time.** If you raise a policy threshold *and* lower the review cutoff in the same save, you won't know which one moved the queue.
* **Watch the histogram, not just the numbers.** A 5-point slider move can shift hundreds of items between zones if your traffic clusters around that score.
* **Hold off on Calibrate until you have a meaningful sample.** A handful of resolved items isn't enough to tune from, so let the queue accumulate first.
* **Pair shadow flagging with [Simulate](/content-moderation/rules#simulate-before-saving).** Shadow flagging previews how a policy would score real traffic; Simulate previews how the rules would route it. Neither touches what your users see.
# Adaptive model optimization
Source: https://docs.moderationapi.com/custom-models/adaptive-model-optimization
Learn how to use the review queue to improve your custom models automatically.
Please reach out to our support team to enable adaptive model optimization for
your account.
Adaptive models can be improved over time by using the review queue. In machine learning, this is called reinforcement learning. This is a way to improve the model by providing feedback on its performance. With Moderation API, you do this using the review queue. Here you can remove incorrect labels, add missing labels, and provide feedback to the model.
**Adaptive models can be useful for**
* Improving accuracy over time
* Adapting to changes in culture or language
* Getting started with a small dataset and improving it over time
## How to provide feedback
When viewing content in the review queue, you can correct the model's decision by removing labels or adding new ones.
Next time you train a custom model, the feedback will be used for improving the model. You can also create a new model and import the corrected data to start with accurate training data.
## Automatically retraining models
If you already have a custom model, you can enable adaptive model optimization. This will automatically retrain the model on an ongoing basis with the feedback you provide in the review queue.
Retraining will happen in the background, and you will be notified when the model has been updated. This way, you can ensure that your model is always up-to-date and accurate.
# Custom models
Source: https://docs.moderationapi.com/custom-models/overview
Create custom models in the dashboard. Tailored to your specific needs and improve accuracy for special use cases.
Create custom models tailored to your specific needs and improve accuracy for special use cases.
Training a custom model may seem daunting at first, but the workflow is userfriendly and designed so anyone can train a model with minimal effort. You can get started in less than 10 minutes.
**Custom models helps you**
* Improve accuracy for special use cases
* Improve models over time with self-learning
* Classify your data with new labels
* Bake all your policies into a single model
Create a model in less than 15 minutes in the dashboard.
## How it works
For example "Allowed" and "Blocked".
Upload a CSV or use content you've already submitted to Moderation API.
If your training data is missing labels, you can annotate it using our
annotation tools.
Start the training and wait for it to finish.
Once your model is trained, you can use it your projects in Moderation API.
## Training data
It is recommended that you have some examples of your content, ideally 200+ examples, but you can get started with as little as 20 examples.
If you don't have any data to upload, you can start collecting it today with Moderation API. Everything submitted using the [text moderation endpoint](/api-reference) will we available as training data for your custom model.
## Self-learning models
Custom models are self-learning, which means that they will improve over time as you use them. This is done by using the [review queue](/review-queues) and correcting the model when it makes a mistake.
## Need help getting started?
If you need any guidance for creating a custom model please [send us a message](https://moderationapi.com/sales) any time. We're happy to help with anything from advice on creating a dataset for your business to implementing the model in your product.
# Using a custom model
Source: https://docs.moderationapi.com/custom-models/using
When a model training has completed, the model will show up alongside all the pre-built models in your project, and you can [add it like any other model](/content-moderation/adding-models).
### Response signature
Custom models have a similar response signature to the [pre-built classifiers](/models/classifier-overview).
Each model returns an object with the detected label and the respective scores. This object is added to the API response under the model's key.
The most probable label. This will always be the label with the highest score.
Returns null if the analyzer fails.
The score of the label. From 0-1 score with 1 meaning a high probability of
being correct.
An object containing all the label scores.
# Concepts
Source: https://docs.moderationapi.com/get-started/concepts
How organizations, projects, channels, and policies fit together in Moderation API.
Moderation API is organized as a four-level hierarchy. Most work happens inside a project; the other levels matter when you start scaling — adding teammates, separating environments, or running multiple products on the same plan.
```mermaid theme={"theme":"nord"}
flowchart TB
subgraph O["Organization"]
direction TB
subgraph P["Project"]
direction TB
A["Authors"]
Q["Content views"]
subgraph C["Channel"]
direction TB
PO["Policies"]
end
end
end
style O fill:#eff6ff,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
style P fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
style A fill:#bfdbfe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
style Q fill:#bfdbfe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
style C fill:#bfdbfe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
style PO fill:#2563eb,stroke:#1e40af,stroke-width:1.5px,color:#ffffff
```
## Organization
The organization is the top-level container. It owns:
* Billing and plan (quota, plan tier, invoices).
* Team members who can access the organization's projects.
* Custom models, trained once and reusable across any channel in any project.
* Wordlists, managed in one place and enabled on the channels that need them.
A user can belong to multiple organizations, and an organization can contain multiple projects.
## Project
Projects scope your content and authors. Everything you submit to the API (content, [authors](/users/overview), [review](/review/overview), [actions](/actions/overview), and channels) belongs to a single project.
Most teams use projects to separate environments like production and staging, or to keep distinct products apart. Each project has its own API key.
When you create a project you fill in:
* A **name** for the dashboard.
* A **domain**: the website or app this project moderates. The AI uses it as context, which sharpens the accuracy of LLM-based policies like [Guidelines](/policies/guidelines).
* A **type**: pick a template to preconfigure channels and content views, or start from scratch.
Templates available today:
| Template | What it sets up |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Marketplace | `listings`, `messages`, and `reviews` channels, plus views for unreviewed listings, flagged reviews, auto-rejected listings, and bad actors |
| Game | `chat` and `usernames` channels |
| Blog | a channel for blog post comments |
| Community | channels for forum-style posts and comments |
| Blank | nothing pre-configured |
Templates create the channels and views. You decide which policies to enable on each channel.
## Channel
Channels live inside a project and represent a specific surface or content type. Common examples are `chat`, `reviews`, `listings`, `usernames`, and `profile-bio`. Each channel is configured independently, so you can apply strict rules to user-to-user messages while keeping product listings looser.
A channel owns:
* **Settings.** Name, key (used to route requests, see below), description, and the [content types](/content-moderation/submit-content#content-types) it accepts (text, image, video, audio).
* **Review thresholds.** The cutoffs at which content gets flagged.
* **Policies.** The rules that score content. Toggle each one on or off per channel. See [Policies](/policies/overview) for the full list.
* **Insights.** Opt-in signals like sentiment and language. They come back alongside policy results but don't affect flagging.
* **Dry-run mode.** When on, API responses always return `flagged: false` so production traffic isn't blocked, but content is still analyzed and still lands in review. Use it to preview a channel before going live.
### Routing requests to a channel
You authenticate with a project's API key. To pick which channel handles each request, the API checks, in order:
1. The `channel` field in the request body (see [Submitting content](/content-moderation/submit-content#channel)).
2. A channel whose content type matches the request's `metaType`. For example, `metaType: "review"` routes to a channel configured for reviews.
3. Otherwise, the newest channel in the project.
You can also set a channel as the project's default route to override step 3.
## Policies
A policy is a single rule that runs against submitted content. Each enabled policy returns one entry in the API response's `policies` array. Because policies are enabled per channel, the same project can run [Toxicity](/policies/toxicity) on chat messages while only running [Privacy](/policies/privacy) on profile bios.
Policies fall into two types:
| Type | What it does |
| ---------------- | ------------------------------------------------------------------------------------ |
| `classifier` | Scores content against labels and returns a probability (e.g. Toxicity, NSFW). |
| `entity_matcher` | Extracts specific entities like URLs, emails, or phone numbers (e.g. Privacy, Spam). |
See the [Policies overview](/policies/overview) for the full catalog.
A few pieces sit above the channel and get attached to it:
* **Custom models** are trained at the organization level and can be enabled on any channel.
* **Wordlists** are also organization-level, then enabled per channel.
* **Guidelines** are written per channel, since the rules usually depend on the surface.
## Authors
Authors (your end users) are scoped to the project, not the channel. A single author can post into multiple channels. For example, the same user might send chat messages, write reviews, and update their profile bio. The author's [trust level](/users/trust-levels) and moderation history aggregate across every channel in the project.
## Review and content views
Most content is handled automatically; what needs a person goes to [review](/review/overview). The inbox collects those items across all channels. [Content views](/review/content-views) are saved, filtered slices of your content — by channel, label, severity, or the action that was taken — and aren't limited to what the inbox holds, so a view can also cover content that was auto-rejected. Views are project-scoped, and a project can have as many as it has distinct reviewer workflows.
Decisions made in review are remembered by the project's [casebook](/casebook/overview), which settles similar content automatically the next time it arrives.
## Quota
Quota is shared across the organization. Each request to the `/moderate` endpoint consumes quota. Most requests cost one unit, though some content types and policies cost more. See [Quota & usage](/account/quota-usage) for the per-policy cost table.
# Get started
Source: https://docs.moderationapi.com/get-started/introduction
Moderation API is a full stack content moderation solution.
## Quickstart
Learn how to get started with Moderation API in 5 minutes.
}
href="/quickstart/typescript"
>
Get started with TypeScript and npm
Get started with Python and pip
Get started with Go
Get started with Ruby
Get started with PHP
Get started with Java
Get started with .NET
## Not a developer?
If you're not a developer, you can still use the API. We have many integrations and no-code solutions that allows you to create projects and moderate content without writing a single line of code.
See our integrations and no-code solutions.
## Guides
Can't find your use case? [Contact us](https://moderationapi.com/sales) and we'll help you get started.
Automate your moderation with the API.
Learn how to use review queues to moderate content.
Define custom rules in natural language for your channel.
Get an overview of the pre-built models available.
Learn how to create a custom model in the dashboard.
# Content moderation for chats
Source: https://docs.moderationapi.com/guides/chat-content-moderation
This page is under construction.
Please reach out at [support@moderationapi.com](mailto:support@moderationapi.com) to get help with this feature.
# Creating a custom model
Source: https://docs.moderationapi.com/guides/creating-a-custom-model
Learn how to create a custom model using Moderation API
Head over to the Model Studio and press the models tab. Here you can create a new custom model. [Model Studio](https://dash.moderationapi.com/models).
## 1. Select your type of model
The first step is to select your type of model. In this guide we’re focused
on creating a classifier so that’s what you want to pick.
## 2. Define your labels
Firstly, you should determine the specific categories or labels you intend to detect. It is advisable to add a limited number of labels as it enhances the performance of the model by reducing complexity. In case you encounter a complex situation, you may consider developing two models instead of a single model with several labels.
When creating your labels you’ll be asked to add a description for the label. This can help your organisation remember the labeling rules later on, but more importantly, it also helps the AI understand how you want to label your data.
Note, once you’ve trained your model, you can no longer change or update your labels.
## 3. Add your training data
To train your model, let’s add some data by providing examples of previously seen text, and the corresponding label that needs to be assigned to them. With Moderation API, this process is made easy, and you get the infrastructure to avoid common pitfalls like duplicate or skewed data.
We have a few guidelines for getting the best results:
* 100 examples or more per. category, though you may get by with less for simple problems.
* Clean your data. Incorrect examples will result in poor performance.
* Diversify the examples to help the model generalise better.
* If the model produces an inaccurate prediction, try to add a few more examples for the given problem.
We provide you with 3 options to add data:
1. **Upload a CSV or excel document.**
2. **Use previous requests stored in moderation API.**
3. **Add or update examples manually.**
Let's go through the use cases for each of the options.
### Upload CSV of Excel document
To upload a large number of examples to your model, you can create a CSV or Excel document and upload it. For instance, you could extract a CSV file from the latest 5000 user posts on your website and upload it here.
When uploading the file, you will be prompted to indicate which column contains the text example, and optionally, which column contains the label. If the data is already labeled, the label column should contain a number that corresponds to the label, not the actual label name. For example, if there are 3 labels, the label column should contain 0, 1, 2, or be empty if there is no label.
If your data is not labeled yet, we will help you annotate it in the next step.
### Use previous requests stored in moderation API
If you've already been using moderation API for a while, and have enabled request logging, you have the option to import these requests for your model. This is the fastest way to add training data to your model.
This is also an excellent way of continously add data and re-train your model to keep it up to date with the real world. For example, once per month you can add the latest requests from your project, and re-train and update your model with the new knowledge.
You can choose to import from any of your projects or from all of your projects. For example if you have a project used for development and one for production, you can import from production only.
### Add or update examples manually
This option should only be used if you don't have any training data, or very little training data.
You can manually type a text and choose the correct label. This way it is possible to create a bunch of examples and get started with your model. In some simple cases with few training examples you might be able to create a decent model this way.
The option can also be used to inspect and update existing examples that have been added using any of the other methods.
## 4. Annotate your data
If your data has already been labeled prior to uploading, you can skip this step. If not, now is the time to annotate your examples with labels.
Data labeling can be extremely time-consuming and often presents a significant challenge for businesses in creating custom machine learning models. With our user-friendly annotation tools, this process becomes significantly easier.
You have two options for annotating your data:
1. **Automated annoation**
2. **Manual annotation**
### Automated Annotation
This feature uses an LLM to annotate your examples based on the labels and descriptions you've previously provided.
Because the auto-annotation uses your label descriptions, tweaking your labels will also change the annotations. You might want to start by auto-annotating 10 examples -> review the result -> tweak the label descriptions -> run auto-annotation again.
Your subscription plan includes a specific number of automated annotations per month, and you can view the remaining allocated amount before initiating the auto annotation process.
Once the auto annotation is complete, you will receive a notification via email.
### Manual annotation
Or semi-manual annotation provides you with an interface to manually label each example.
This tool is composed of several parts, each designed to help you with the annotation process:
1. The current example you are labeling is displayed at the top.
2. Your progress is shown with the count of examples you have already labeled.
3. The example text is provided, which can be altered to your preference.
4. The appropriate label can be selected from this section. The tool will attempt to predict the correct label upon loading the example. 5. If the prediction is accurate, you can save and proceed to the next example.
5. An estimated similarity score between the labels and example is viewable here. The tool will suggest the label with the highest similarity, although please note that this similarity score does not reflect the performance of your model.
## 5. Start the training
After annotating your data, proceed to the training process.
### Reviewing training data
Before beginning the training, review the model’s details, such as the total amount of training data, annotated examples, and the number of examples per label. This helps you identify any underrepresented labels and allows you to adjust them as needed. Aim for a balanced distribution of examples across all labels.
Keep in mind that the quality and quantity of your training data significantly affect your model’s performance.
You always have the option to add more data and retrain the model. If your data evolves over time, we recommend updating the model regularly. When retraining, the system replaces the old model with the newly trained one upon completion.
### Time to train
The training may take some time, depending on the amount of data and the complexity of your model. Throughout the training process, the system learns from your annotated examples and develops a model capable of classifying unseen text based on the labels you defined earlier. Once the training is complete, you will receive an email notification.
## 6. Reviewing your model
Once your training is complete, you'll want to integrate it into your project, but first, take a moment to review the performance.
### F1-score
Start by checking the F1-score, a popular metric for gauging classification models. It combines precision and recall into one value for a balanced assessment of your model's abilities. The F1-score ranges from 0 to 1, with 1 being the ideal balance between precision and recall.
A perfect F1-score of 1 is uncommon for real-world problems. A 0.7 score can be great for certain problems. The minimum acceptable F1-score depends on the specific situation, so it can't be universally defined.
If your model has a relatively low F1-score, try adding more data to improve its performance. Focus on examples the model struggles with to better capture patterns.
Alternatively, if more data isn't possible, adjust the label probabilities threshold. For instance, if your threshold is at 0.5, consider raising it to 0.7 or higher. This will only assign a label if its predicted probability passes the new threshold, resulting in potentially better performance in real-world situations.
### Test run
You can also test-run the model to quickly test various examples. This is useful if you want to make sure it handles certain scenarios before you implement it in your project.
## All done!
That's it! You've created your first custom model. Now it's time to integrate it into your project.
If you need any help or have any questions, please don't hesitate to reach out to us.
# Content moderation for marketplaces
Source: https://docs.moderationapi.com/guides/marketplace-content-moderation
This page is under construction.
Please reach out at [support@moderationapi.com](mailto:support@moderationapi.com) to get help with this feature.
# Designing a moderation workflow
Source: https://docs.moderationapi.com/guides/moderation-workflow
This page is under construction.
Please reach out at [support@moderationapi.com](mailto:support@moderationapi.com) to get help with this feature.
# How to set up self-learning models
Source: https://docs.moderationapi.com/guides/self-learning
This page is under construction.
Please reach out at [support@moderationapi.com](mailto:support@moderationapi.com) to get help with this feature.
# Handling User-Flagged Content
Source: https://docs.moderationapi.com/guides/user-flagged-content
Learn how to handle user-flagged content with the Moderation API
## Introduction
In this guide, we'll demonstrate how to enable users to report other profiles within a fictional dating application named **Wizard Dating**.
### **Goals**
* **View an overview of reported profiles**
* **Allow users to report profiles**
* **Review and remove reported profiles from the application**
* **Sort reported profiles to prioritize the most problematic ones**
When users report a profile, we'll add it to a review queue so that an admin can assess it. If the admin decides to remove the profile, we'll eliminate it from our application.
***
## Setting Up the Dashboard
First, let's set up the dashboard by creating the necessary components. We'll need to create:
1. A project for analyzing profiles
2. An action for users to report profiles
3. A saved review-queue view to display reported profiles
4. An action for moderators to remove profiles
### Create a Project
We'll start by creating a new project called *Wizard Profiles* and adding several models to it: `toxicity`, `nsfw`, `sentiment`, `pii`, and `spam`.
We will keep the flagging thresholds at their default values for now.
### Create a Reporting Action
Navigate to [your actions](https://dash.moderationapi.com/moderation/actions) to create a new action.
1. Create an action named `Report Profile` that will be used to report user profiles.
2. Since this action will only be invoked by our application code, select **"Hide action from dashboard"** to prevent it from appearing in any queues.
3. Additionally, check **"Allow text input for value"** so users can add comments to their reports.
4. Note the action key `report_profile` for later use.
5. Select queue behavior: **"Action unresolves item (re-add to queue)"**: to always re-add the profile to the queue even if a moderator has resolved it before. Otherwise leave it at **"Action does not resolve item"** to only review the profile once.
### Create a Reported Profiles View
Next, we'll create a [view](/review/content-views) so reported profiles have their own place in the review queue.
1. Open the Review inbox and click **Filter** in the [filter bar](/review/content-views#filter-properties).
2. Add an **Action taken** filter for the `Report Profile` action we created earlier.
3. Remove any flag status or recommendation filters, so reports appear even when our models didn't flag the profile.
4. Optionally add a **Date** filter such as "Last 30 days" to maintain focus.
5. Open **Save → Create new view…** and name the view *Reported Profiles*.
### Create a Removal Action
Now that the view shows reported profiles, we'll add an action to remove profiles from our application.
1. Create an action named `Remove Profile`.
2. Configure it to appear only in the newly created *Reported Profiles* view.
3. Enable **"Action resolves items"** so that the profile is removed from the queue once the action is executed.
4. Set up a webhook to call our application servers at `https://wizard-dating.com/webhooks` to handle the removal of the profile from the application.
***
## Application Code
Next, we'll implement the necessary code in our application to handle user reports and profile removals.
We'll use the Moderation API's [Node SDK](https://www.npmjs.com/package/@moderation-api/sdk) to interact with the API from our application.
### Prerequisites
#### Environment Variables
Create a `.env` file in your project root with your project API key:
```bash theme={"theme":"nord"}
MODERATION_API_KEY=your_project_api_key
```
#### Dependencies
Install the Moderation API's Node SDK using your preferred package manager:
```bash npm theme={"theme":"nord"}
npm install @moderation-api/sdk
```
```bash yarn theme={"theme":"nord"}
yarn add @moderation-api/sdk
```
```bash pnpm theme={"theme":"nord"}
pnpm add @moderation-api/sdk
```
### Instantiating the SDK
To use the Moderation API in our application, we'll need to instantiate it with our project's API key.
```typescript /lib/moderation.ts theme={"theme":"nord"}
import ModerationAPI from "@moderation-api/sdk";
// Use environment variable MODAPI_SECRET_KEY
export const moderationApi = new ModerationAPI();
// Or pass key explicitly
// export const moderationApi = new ModerationAPI({
// secretKey: process.env.MODERATION_API_KEY,
// });
```
### Submitting Profiles for Analysis
When a user creates or updates a profile, we'll submit it for analysis using the `/moderate` endpoint. This allows us to detect any issues with the profile content.
```typescript Create Profile theme={"theme":"nord"}
import { moderationApi } from "./lib/moderation";
import db from "./lib/db";
export const handleCreateProfile = async ({ bio, profilePicture }) => {
const profile = await db.profile.create({
data: {
bio,
profilePicture,
},
});
const result = await moderationApi.content.submit({
content: {
type: "object",
data: {
bio: {
type: "text",
text: bio,
},
profilePicture: {
type: "image",
url: profilePicture,
},
},
},
contentId: profile.id,
authorId: profile.id,
// Optional: Add the URL to quickly view the profile from the queue
metadata: {
url: profile.url,
},
});
// Check if profile was flagged
if (result.evaluation.flagged) {
// Handle flagged profile (e.g., require review)
}
return profile;
};
```
```typescript Update Profile theme={"theme":"nord"}
import { moderationApi } from "./lib/moderation";
import db from "./lib/db";
export const handleUpdateProfile = async ({ id, bio, profilePicture }) => {
const profile = await db.profile.update({
where: { id },
data: { bio, profilePicture },
});
const result = await moderationApi.content.submit({
content: {
type: "object",
data: {
bio: {
type: "text",
text: bio,
},
profilePicture: {
type: "image",
url: profilePicture,
},
},
},
contentId: id,
authorId: id,
// Optional: Add the URL to quickly view the profile from the queue
metadata: {
url: profile.url,
},
});
// Check if profile was flagged
if (result.evaluation.flagged) {
// Handle flagged profile (e.g., require review)
}
return profile;
};
```
*In this example, we are not acting on the analysis results, but you could use the data to hide flagged profiles or return an error to the user.*
### Adding Report Functionality to Our Application
We'll add a function to call the `/actions/execute` endpoint to report a profile. This function should be exposed to users through your application's UI.
```typescript Handle Report Profile theme={"theme":"nord"}
import { moderationApi } from "./lib/moderation";
export const handleReportProfile = async (profile, reason) => {
const actionKey = "report_profile";
const { success } = await moderationApi.queueActions.execute(actionKey, {
contentIds: [profile.id],
value: reason,
// Optional: Find the queue ID in the URL of the queue's detail view. Add this if you always want to re-add the profile to the queue even if a moderator has resolved it before.
queueId: "6776a700c62f7bc4e7ba57bb",
});
return success;
};
```
### Handling the Webhook
We'll implement a webhook handler to process the `Remove Profile` action. The handler verifies the `modapi-signature` header, then dispatches on the event `type`.
```javascript /pages/api/webhooks.js theme={"theme":"nord"}
import crypto from "crypto";
import { buffer } from "micro";
import db from "../../lib/db";
const handler = async (req, res) => {
const rawBody = (await buffer(req)).toString("utf8");
const signatureHeader = req.headers["modapi-signature"];
// Verify the signature with HMAC-SHA256
const expected = crypto
.createHmac("sha256", process.env.MODAPI_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const sig = Buffer.from(signatureHeader || "", "utf8");
const dig = Buffer.from(expected, "utf8");
if (sig.length !== dig.length || !crypto.timingSafeEqual(sig, dig)) {
return res.status(401).send();
}
const event = JSON.parse(rawBody);
// `queue_item.action` fires for custom actions on a queue item.
// The action lives at the root of `data.object`; the item is nested.
if (
event.type === "queue_item.action" &&
event.data.object.key === "remove_profile"
) {
const { item } = event.data.object;
// Remove the user from our application
await db.user.update({
where: { userId: item.id },
data: { banned: true },
});
}
return res.status(200).send();
};
// Disable body parser to access raw body
export const config = {
api: {
bodyParser: false,
},
};
export default handler;
```
For more information, refer to the [webhook documentation](/actions/webhooks).
***
## Using Our Review Queue
With everything set up, we can start using our review queue to manage reported profiles.
Upon opening the review queue, we'll see reports submitted by users on our Wizard Dating app. We'll review each report to decide whether to remove the profile or keep it.
### Focusing on the Worst Offenders
To prioritize profiles that have also been flagged by our model analysis, we can filter the view.
1. Open **Filter** in the filter bar and select the labels you want to focus on, such as `UNSAFE`.
> **Tip:** You can also click on the labels in the chart to apply the filter.
2. After setting the filter, you'll see items labeled as `UNSAFE`.
### Removing a Profile
1. Click on a queue item to open its detail view, where you can see the profile content and metadata.
2. Review the flags and activity history, including when the profile was submitted, reported, and the reasons provided by users.
3. If the profile violates guidelines, click the **"Remove Profile"** action. This will trigger the webhook to remove the profile from the application and resolve the item in the queue.
### Keeping a Profile
1. Reset the filter to view all remaining items in the queue.
2. Select a profile that has been reported but not flagged by the models.
> **Example:** *"I'm actually a muggle but I'm looking for something magical."*
3. If the profile appears appropriate, click the **"Resolve"** button to remove it from the queue without taking further action.
4. Repeat the process for other profiles as needed.
***
## All Done!
The review queue is now empty, and we've successfully handled all reported profiles.
### Accomplishments:
* **Enabled users to report profiles**
* **Set up a review queue to manage reports**
* **Implemented a webhook to remove profiles from the application**
## Next Steps
* **Invite moderators to your queue:** Expand your moderation team to handle more reports efficiently.
* **Use the data to train a model:** Enhance your models to better recognize inappropriate profiles.
* **Implement an automated policy:** Automatically remove profiles that receive multiple flags to streamline moderation.
# Model Studio
Source: https://docs.moderationapi.com/model-studio/overview
Use the Model Studio to create custom models for your moderation policies.
The Model Studio is a powerful tool that allows you to create custom models for your use case.
#### 3 types of models
These are pre-built models that can be used to analyze content without any
additional training.
These are models that are trained on user-provided data to analyze content
specific to their use case.
Prevent specific words and phrases from being used on your platform.
| Model | Needs training data | Customizable | Accuracy | Effort |
| -------------------- | ------------------- | ------------ | -------- | ------ |
| **Pre-built models** | No | No | High | Low |
| **Custom models** | Yes | Yes | Highest | High |
| **Wordlists** | No | Yes | High | Low |
For platform-specific rules without training data, use [Guidelines](/policies/guidelines) — natural-language policies you configure per channel.
# Guidelines
Source: https://docs.moderationapi.com/policies/guidelines
Define custom moderation rules in natural language. Each guideline is evaluated by an LLM against submitted content and returned as its own policy.
Guidelines let you define custom rules in plain English without training a model. Write what isn't allowed, give the rule a key, and the LLM scores every submission against it. You can add as many as you need — they're all evaluated in a single LLM call per request.
Use guidelines when one of the [pre-built policies](/policies/overview#available-policies) doesn't quite cover what you want, but the rule is simple enough to describe in a sentence or two.
## Shape
Each guideline has two fields:
| Field | Limit | What it is |
| -------------- | --------- | ----------------------------------------------------------------------------------- |
| `key` | 100 chars | A unique identifier for the rule on this channel (e.g. `no-spam`, `be-respectful`). |
| `instructions` | 750 chars | A natural-language description of what is not allowed. |
## In API responses
Every guideline shows up as its own entry in `response.policies`. The `id` is `guideline/`, the `type` is `classifier`, and you read `probability` / `flagged` like any other classifier:
```json theme={"theme":"nord"}
{
"policies": [
{
"id": "guideline/no-spam",
"type": "classifier",
"probability": 0.92,
"flagged": true
},
{
"id": "guideline/be-respectful",
"type": "classifier",
"probability": 0.08,
"flagged": false
}
]
}
```
```javascript theme={"theme":"nord"}
const noSpam = response.policies.find(p => p.id === "guideline/no-spam");
if (noSpam?.flagged) {
await sendToReview(content);
}
```
## Writing good instructions
A few patterns that work well:
* **Lead with the violation, not the policy.** `Posts that ask users to DM, follow on another platform, or share contact info` beats `Anti-spam rule for off-platform redirection`.
* **Be specific about edge cases.** If "promoting other products" should allow users mentioning tools they use, say so: `Promoting other products or services. Mentioning tools a user personally uses is allowed.`
* **One concept per guideline.** Split unrelated rules into separate guidelines so you can flag them independently and tune thresholds per rule.
* **Keep it under \~300 characters.** Longer instructions don't make the LLM more accurate — they make it slower and harder to debug.
## Configuring guidelines
Guidelines are configured in the dashboard, per channel — open the channel's **Policies** tab and add them under **Guidelines**. They run on text and audio content.
## Project context
Guidelines are evaluated with your **project context** in scope. That's the description you set under **Configure → Context** in the dashboard — typically a short paragraph about what your platform is, who uses it, and the kind of content you expect. It's passed to the LLM alongside your guideline instructions so the rules are interpreted in the right setting.
For example, a guideline like `No promoting other platforms` will behave differently for a developer forum (where mentioning GitHub is normal) than for a dating app (where directing users off-platform is a red flag). A well-written project context lets the LLM make that call without you spelling out every exception in the guideline itself.
See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape, and [Optimizing accuracy](/content-moderation/optimizing-accuracy) for guidance on tuning thresholds.
# Illicit & Regulated
Source: https://docs.moderationapi.com/policies/illicit
Detect illegal activities and regulated content categories like drugs, alcohol, firearms, gambling, adult products, and crypto.
Illicit policies cover content that's either outright illegal or falls into categories that are regulated, age-restricted, or commonly disallowed by platform terms. Most also support image, video, and audio so you can moderate uploads as well as text.
## Policies
| `id` | Type | Supported | What it does |
| ------------------ | ------------ | ------------------------- | --------------------------------------------------------------------------------------------------------- |
| `illicit` | `classifier` | text, audio | General detector for illicit or illegal content and activities. |
| `illicit_drugs` | `classifier` | text, image, video, audio | Illegal drugs, narcotics, controlled substances. |
| `illicit_alcohol` | `classifier` | text, image, video, audio | Illegal alcohol sales or underage drinking. |
| `illicit_firearms` | `classifier` | text, image, video, audio | Illegal firearms, weapons, weapon sales. |
| `illicit_tobacco` | `classifier` | text, image, video, audio | Illegal tobacco sales or underage tobacco/vape use. |
| `illicit_gambling` | `classifier` | text, image, video, audio | Illegal gambling activities. |
| `cannabis` | `classifier` | text, audio | Cannabis and marijuana content — split out so you can allow or deny independently of other drugs. |
| `adult` | `classifier` | text, audio | Adult products, escort solicitation, pornography, adult websites. |
| `crypto` | `classifier` | text, audio | Cryptocurrency, NFT, and blockchain promotions — useful for filtering shilling and scam-adjacent content. |
## Reading the result
```javascript theme={"theme":"nord"}
const cannabis = response.policies.find(p => p.id === "cannabis");
if (cannabis?.flagged) {
// Block in regions where it's illegal; allow elsewhere.
}
```
These categories are independent. Enable only the ones that match your jurisdiction and product. For example, a cannabis-focused community would disable `cannabis` but keep `illicit_drugs` enabled.
See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape.
# NSFW
Source: https://docs.moderationapi.com/policies/nsfw
Detect sexual content, flirtation, profanity, violence, and self-harm across text, image, video, and audio.
NSFW (not safe for work) policies cover content that's inappropriate for general audiences — explicit material, graphic violence, profanity, and content related to self-harm.
## Policies
| `id` | Type | Supported | What it does |
| ------------ | ------------ | ------------------------- | ------------------------------------------------------------------------------------------------- |
| `sexual` | `classifier` | text, image, video, audio | Sexually explicit or suggestive content. |
| `flirtation` | `classifier` | text, audio | Compliments on looks, subtle innuendo, pickup lines, flirtatious behavior. Lighter than `sexual`. |
| `profanity` | `classifier` | text, audio | Swearing, vulgar language, offensive words. |
| `violence` | `classifier` | text, image, video, audio | Violent content, threats, or graphic violence. |
| `self_harm` | `classifier` | text, image, video, audio | Self-harm, suicide, and eating-disorder content. |
## Reading the result
```javascript theme={"theme":"nord"}
const sexual = response.policies.find(p => p.id === "sexual");
if (sexual?.flagged) {
// Send to review queue or reject outright depending on threshold.
}
```
`self_harm` content often warrants a different response than other NSFW flags — for example, surfacing a support resource link rather than just rejecting the post. Use the `recommendation.action` plus the policy `id` to branch.
See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape.
# Policies
Source: https://docs.moderationapi.com/policies/overview
Reference for the moderation policies you can enable in a channel and how each one scores content.
A policy is a single rule the API runs against submitted content. Each channel has its own set of enabled policies, and every API response includes a `policies` array with one entry per enabled policy.
```javascript theme={"theme":"nord"}
const toxicityPolicy = response.policies.find(p => p.id === "toxicity");
if (toxicityPolicy?.flagged) {
// act on the result
}
```
See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape.
## Policy types
Every policy returns a `type` that tells you how to read its result:
| Type | What it does | Example fields |
| ---------------- | --------------------------------------------------------------------------- | ------------------------- |
| `classifier` | Scores content against one or more labels and returns a probability | `probability`, `labels[]` |
| `entity_matcher` | Extracts specific entities (URLs, emails, phone numbers, etc.) from content | `matches[]`, `signals` |
## Available policies
Define custom rules in natural language. Each guideline is evaluated by an LLM and returned as its own policy.
Your own lists of words and phrases, matched with semantic understanding. Block them, require them, or just extract them.
Personal information detection, intent to share contact details, PII masking, and face detection in images.
Toxicity, severe toxicity, and hate-based content including discrimination and extremism.
Sexual content, flirtation, profanity, violence, and self-harm across text, image, video, and audio.
Drugs, alcohol, firearms, gambling, adult products, cannabis, crypto, and other regulated categories.
Spam, low-quality content, self-promotion, code abuse, phishing, and URL extraction.
Real-time risk scoring for URLs — phishing, malware, brand impersonation, credential harvesting.
Lookalike characters, mixed alphabets, invisible characters, and Zalgo used to disguise banned words.
Political and religious content for platforms that want to keep discussions on-topic.
Insights like `sentiment` and `language` aren't policies — they're returned in the separate `insights` array and don't affect flagging. See [Understanding API responses](/content-moderation/acting-on-responses#use-insights).
# Privacy
Source: https://docs.moderationapi.com/policies/privacy
Detect and mask personal information, contact details, and attempts to share them in obfuscated ways.
Privacy policies catch sensitive user information before it reaches your database, public feed, or other users. They cover detection (does this content contain PII?) as well as masking (redact it from the stored content).
## Policies
| `id` | Type | Supported | What it does |
| ---------------------- | ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `personal_information` | `classifier` | text, audio | Flags content that contains emails, phone numbers, addresses, or other sensitive numbers. |
| `intent_to_share` | `classifier` | text, audio | Flags attempts to share contact details — directly or obfuscated (e.g. "find me on Instagram"). Experimental |
| `pii` | `entity_matcher` | text, audio | Extracts specific PII entities (email, phone, address, etc.) and can return masked content with those entities redacted. |
| `face_detection` | `classifier` | image | Flags images by how many faces they contain. See [Face detection](#face-detection). |
## Face detection
Face detection flags images with a single rule you write as a sentence:
> Flag images that contain **at least** / **fewer than** **N** faces.
Both directions are useful, which is why the rule reads this way rather than as a simple on/off:
* **At least 1** — flag any image showing a person. Use it where faces shouldn't appear at all, like product photos or document uploads.
* **Fewer than 1** — flag images with no face. Use it where a face is required, like verifying that a profile picture is actually of someone.
* **At least 3** — flag group photos while letting individual portraits through.
Configure it on a channel's **Policies → Privacy** page. Two settings:
| Setting | What it does |
| --------- | -------------------------------------------------------------------------------- |
| Flagging | The rule itself — the comparator (`at_least` or `fewer_than`) and the face count |
| Threshold | How confident the detector must be before a face counts |
`probability` reports confidence that the rule is satisfied, so an absence match ("fewer than 1 face, and there are none") reports high confidence rather than zero. Detected faces are returned on the policy's `data` field.
Face detection runs on images only. It doesn't identify anyone — it counts faces and returns no identity information.
## Reading the result
```javascript theme={"theme":"nord"}
const pii = response.policies.find(p => p.id === "pii");
pii?.matches?.forEach(match => {
console.log(`${match.match} at ${match.span[0]}–${match.span[1]}`);
});
if (response.content.masked) {
await store(response.content.modified); // PII redacted
}
```
See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape and masking behavior.
# Spam & Security
Source: https://docs.moderationapi.com/policies/spam
Detect spam, self-promotion, code abuse, phishing attempts, and mask URLs in user-generated content.
Spam and security policies catch content that's trying to manipulate, deceive, or exploit your platform — from low-effort promotion all the way to active phishing and code injection attempts.
For URL-specific risk scoring, see [URL Risk](/policies/url-risk). For lookalike characters and other Unicode evasion, see [Unicode spoofing](/policies/unicode-spoofing).
## Policies
| `id` | Type | Supported | What it does |
| ------------------ | ---------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `spam` | `classifier` | text, audio | Spam, repetitive content, unsolicited messages. |
| `low_quality` | `classifier` | text, audio | Low-effort content — too short, heavily repetitive, or duplicated sentences. See [Low-quality content](#low-quality-content). |
| `self_promotion` | `classifier` | text, audio | Self-promotional content and advertising. Experimental |
| `code_abuse` | `classifier` | text, audio | Malicious code, code injection attempts, and abuse of code features. Useful as a guardrail in front of LLM agents. |
| `phishing` | `classifier` | text, audio | Phishing attempts and scam messages. Preview Requires conversation context to be enabled on the channel. |
| `url` | `entity_matcher` | text, audio | Extracts URLs from content and can mask them in the returned content. |
| `unicode_spoofing` | `classifier` | text | Lookalike characters, mixed alphabets, invisible characters, and Zalgo. See [Unicode spoofing](/policies/unicode-spoofing). |
## Low-quality content
`spam` targets promotional, scam, and templated content. It doesn't catch the other kind of noise: posts that carry no information at all. `low_quality` fills that gap and catches three patterns:
| Pattern | Example |
| ------------------ | -------------------------------------------------------------------------------- |
| Too short | `ok`, `+1`, a one-word reply below your minimum word count |
| Heavily repetitive | `buy buy buy buy now` — few unique words, or the same word repeated back to back |
| Duplicated | The same sentence or line pasted several times to pad a post |
The strongest of the three decides the score, so any one pattern on its own is enough to flag.
Configure it on a channel's **Policies → Spam** page:
| Setting | What it does |
| ---------------------- | --------------------------------------------------------------------------- |
| Minimum words | Content with fewer words than this is flagged as too short. Defaults to `3` |
| Repetition sensitivity | The detection threshold for the repetition and duplication signals |
| Flagging | Flag or shadow flag |
The policy is fully deterministic — no model runs, so it adds no usage cost and no latency. It's a cheap first line of defense in front of the policies that do call a model.
The minimum word count is a hard rule: content below it always flags, whatever the sensitivity slider says. The slider only affects the repetition and duplication signals.
## Reading the result
```javascript theme={"theme":"nord"}
const spam = response.policies.find(p => p.id === "spam");
if (spam?.flagged) {
await sendToReview(content);
}
const urls = response.policies.find(p => p.id === "url");
urls?.matches?.forEach(m => console.log(m.match));
```
`phishing` looks at the conversation as a whole, not just the latest message. Enable conversation context on the channel and pass prior turns when you submit content — otherwise the policy can't run.
See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape.
# Topics
Source: https://docs.moderationapi.com/policies/topics
Detect content about politics or religion when you want to keep discussions on-topic for your platform.
Topic policies don't flag content as harmful — they flag it as off-topic for products that explicitly want to keep certain subjects out of their community. Pair them with the `recommendation` action of your choice (typically `review` rather than `reject`).
## Policies
| `id` | Type | Supported | What it does |
| ----------- | ------------ | ----------- | -------------------------------------------- |
| `political` | `classifier` | text, audio | Political content, discussions, or opinions. |
| `religion` | `classifier` | text, audio | Religious content, discussions, or opinions. |
## Reading the result
```javascript theme={"theme":"nord"}
const political = response.policies.find(p => p.id === "political");
if (political?.flagged) {
await sendToReview(content);
}
```
These categories are intentionally broad. They're best used in environments with a clear scope — a cooking community, a developer forum, a customer support channel — where any political or religious tangent is off-topic regardless of tone.
See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape.
# Toxicity & Hate
Source: https://docs.moderationapi.com/policies/toxicity
Detect toxic language, severe toxicity, and hate-based content including discrimination, racism, and extremism.
These policies flag content that's hostile to readers or targeted at protected groups. They run on both sides of a conversation — what users post and what your bots or assistants reply.
## Policies
| `id` | Type | Supported | What it does |
| ----------------- | ------------ | ------------------------- | --------------------------------------------------------------------------------------------------------- |
| `toxicity` | `classifier` | text, audio | General-purpose toxicity detection: insults, harassment, hostile language. |
| `toxicity_severe` | `classifier` | text, audio | A stricter sub-classifier for severe toxicity. Useful when you want to distinguish "rude" from "abusive." |
| `hate` | `classifier` | text, image, video, audio | Hate speech, discrimination, racism, and extremism — including image and video content. |
## Reading the result
```javascript theme={"theme":"nord"}
const toxicity = response.policies.find(p => p.id === "toxicity");
if (toxicity?.flagged) {
console.log(`Toxicity ${toxicity.probability * 100}% confidence`);
}
const severe = toxicity?.labels?.find(l => l.id === "severe");
if (severe?.flagged) {
// Treat severe toxicity differently — e.g. auto-reject instead of review.
}
```
See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape.
# Unicode spoofing detection
Source: https://docs.moderationapi.com/policies/unicode-spoofing
Catch homoglyph and Unicode evasion — lookalike characters, mixed alphabets, invisible characters, and Zalgo used to slip banned words past filters.
Spammers disguise words by swapping letters for identical-looking characters
from other alphabets. To a person the text reads normally, but the underlying
characters are a mix — so a banned word sails past keyword filters, and some
networks (mobile carriers especially) treat the message as deliberate evasion
and block it outright.
```text What a person sees theme={"theme":"nord"}
Соngrаts! You won a FREE сrурtо рrіzе — сlаіm it now
```
```text What's really there theme={"theme":"nord"}
Соngrаts → the С, о and а are Cyrillic, not Latin
сrурtо → Cyrillic с, у, р, о mixed into "crypto"
рrіzе → Cyrillic р, і mixed into "prize"
```
The Unicode spoofing policy catches this and returns a cleaned‑up version of the
message so you can block it — or send the normalized text instead.
Detection is powered by our open‑source
[`@moderation-api/unicode-spoofing`](https://www.npmjs.com/package/@moderation-api/unicode-spoofing)
package, based on the official Unicode data and refreshed regularly as Unicode
publishes updates.
## How it works
The policy analyzes a message **word by word**. Instead of flagging any
non‑English character — which would break legitimate multilingual content — it
looks for evidence that a character was chosen to *disguise* itself as something
else, by comparing each character against Unicode's catalog of known lookalikes.
Genuine foreign words have no lookalike to hide behind, so they pass through
untouched.
## Signals
The policy reports five independent signals. Each shows up as a **label** on the
result, and each can be turned on or off individually.
| Signal | What it catches | Example |
| ----------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `mixed_script` | One word blends alphabets — reads as English, but some letters are from another script | `busіnеss` — the `і` and `е` are Cyrillic |
| `confusable_word` | A whole word spelled in lookalikes that reads as a normal word | `НОТ` (all Cyrillic) reads as `HOT` |
| `invisible` | Zero‑width or hidden characters wedged inside a word to split a keyword | a zero‑width space hidden inside `free` |
| `zalgo` | Text buried under stacked combining marks | `Z̸a̛l̷g̷o̶` reads as `Zalgo` |
| `illegal` | Stray control characters that never occur in real text | a hidden control character spliced into the message |
Legitimate multilingual content is left alone automatically. A fully Russian,
Greek, Japanese, Arabic, or Hebrew message — or a foreign name inside an English
sentence — does not flag, because genuine foreign words have no lookalike to
disguise as. There's no language allowlist to maintain.
## Enable and configure
Unicode spoofing is configured entirely from the dashboard.
In your channel, go to **Policies → Spam** and find the **Unicode spoofing**
card.
Toggle **Enable detection** on.
Pick **Flag** to act on detections, or **Shadow flag** to observe without
flagging while you tune. Shadow‑flagged content is visible in queues configured
to show it.
Under **Signals**, every signal is on by default. Turn one off to keep detecting
and reporting it while stopping it from flagging on its own — useful if, say, you
want to see Zalgo without acting on it.
## In the review queue
Spoofed text is designed to look ordinary, so a flagged item can read as
perfectly clean to the moderator reviewing it. To make the detection reviewable,
the item detail view highlights the exact tokens that tripped the policy.
None of the highlighted words above would match a wordlist entry: two of them
mix Cyrillic letters into otherwise Latin words, and the third has a zero-width
character splitting it in half.
Each highlight carries the evidence behind it:
* **Which signal fired** — mixed script, look-alike, invisible, or Zalgo.
* **Which scripts are mixed** into the word.
* **What it looks like** — the word after look-alike characters are folded away,
so you can see the banned word it was disguising.
* **The exact code points** involved, as `U+XXXX`.
Invisible characters have nothing to highlight on their own, so the highlight
widens by a character on each side to stay visible.
Highlights render wherever queue content is shown: the single item view, the
conversation context view, and alongside a translation.
The original characters are what we store, so the evidence is reconstructed
from the item itself. Items moderated before this shipped were stored already
cleaned up, and show an "original unavailable" note instead of highlights.
## Reading the result
When enabled, the policy appears in the response's `policies` array. See
[Understanding API responses](/content-moderation/acting-on-responses) for the
full response shape.
Whether an enabled signal fired.
`1` when an enabled signal fired, `0` otherwise. In shadow flag mode this is
still `1` while `flagged` stays `false`.
One entry per signal — `{ id, probability, flagged }`. A label with
`probability: 1` but `flagged: false` means that signal was detected but is
turned off in your settings.
For object content, the field keys where a signal fired. Omitted for plain
text.
Total words analyzed.
How many words carried a signal.
Up to 10 example offending words, for a quick preview in review queues. The
complete cleaned‑up text is always available in `content.modified`.
Two more fields surface outside the `policies` array — the top‑level
`evaluation.unicode_spoofed` flag and the cleaned‑up `content.modified` text.
Both come from normalization and are set whether or not this policy is enabled.
See [Normalization](#normalization) below.
## Example
```bash cURL theme={"theme":"nord"}
curl https://api.moderationapi.com/v1/moderate \
-H "Authorization: Bearer $MODERATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": { "type": "text", "text": "Соngrаts! You won a FREE сrурtо рrіzе — сlаіm it now" }
}'
```
```javascript SDK theme={"theme":"nord"}
import ModerationApi from '@moderation-api/sdk';
const moderationApi = new ModerationApi();
const result = await moderationApi.content.submit({
content: {
type: 'text',
text: 'Соngrаts! You won a FREE сrурtо рrіzе — сlаіm it now',
},
});
```
```json Response theme={"theme":"nord"}
{
"content": {
"id": "msg_123",
"masked": false,
"modified": "Congrats! You won a FREE crypto prize — claim it now"
},
"evaluation": {
"flagged": true,
"flag_probability": 1,
"severity_score": 0.34,
"unicode_spoofed": true
},
"recommendation": {
"action": "reject",
"reason_codes": ["rule_match"]
},
"policies": [
{
"id": "unicode_spoofing",
"type": "classifier",
"probability": 1,
"flagged": true,
"labels": [
{ "id": "mixed_script", "probability": 1, "flagged": true },
{ "id": "confusable_word", "probability": 0, "flagged": false },
{ "id": "invisible", "probability": 0, "flagged": false },
{ "id": "zalgo", "probability": 0, "flagged": false },
{ "id": "illegal", "probability": 0, "flagged": false }
],
"data": {
"counts": { "words_total": 10, "words_affected": 4 },
"sample_words": ["Соngrаts", "сrурtо", "рrіzе", "сlаіm"]
}
}
]
}
```
## Acting on detections
The policy feeds the [rules engine](/content-moderation/rules) and
[review queues](/review/overview) like any other, so you decide what
happens to spoofed content:
* **Block it.** Add a rule that rejects content when Unicode spoofing is flagged.
The policy also raises the severity score, so channels using severity‑based
recommendations lean toward rejection automatically.
* **Route signals differently.** Each signal is available separately in rules —
for example, reject clear lookalike attacks while sending Zalgo to human
review.
* **Send the clean version instead.** If your goal is deliverability rather than
blocking, forward the cleaned‑up text from `content.modified` so the message
goes out in a form that won't be blocked.
## Normalization
Separately from the policy, your channel has a **Unicode spoof detection**
setting under **Content → Text**, on by default. It rewrites disguised text
*before any other policy runs*, so the rest of your setup sees the real message
instead of the disguise.
With it on, a message like `Buy сhеар vіаgrа` reaches your wordlists and
classifiers as `Buy cheap viagra` — so a banned word wrapped in lookalikes still
gets caught by the wordlist that was already looking for it.
Normalization rewrites only the words that carry a signal: lookalikes become
their real letters, invisible characters are removed, and Zalgo is stripped.
Text that isn't disguised is passed through untouched, byte for byte, so
legitimate multilingual content survives normalization intact.
Normalization applies to text. Image, video, audio, and URL content is
unaffected. For object content, each field is normalized on its own.
### The setting and the policy together
They're independent, and each is useful without the other:
| Setting | Policy | What you get |
| ------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| On | Off | Disguised text is silently normalized so your other policies catch what's underneath. `unicode_spoofed` tells you it happened. |
| On | On | Same, plus spoofing becomes its own flaggable, routable signal. |
| Off | On | Spoofing is detected and reported, but the text your other policies see is left as the sender wrote it. |
| Off | Off | No detection. `unicode_spoofed` is omitted from the response. |
The common setup is both on: keep the setting on so spoofing can't hide banned
words from your other policies, and enable the policy to act on the spoofing
itself.
Because normalization runs first, the policy's own detection is unaffected by
it — enabling one never hides results from the other.
### In the response
Normalization surfaces in two fields, whether or not the policy is enabled:
`true` when spoofing was detected anywhere in the message. Omitted entirely
when both the channel setting and the policy are off, since nothing analyzed
the text.
The normalized message, present whenever normalization changed the text. If
masking is also enabled, this field carries the masked *and* normalized
result.
Forward `content.modified` when your goal is deliverability rather than
blocking — it's the same message in a form that won't trip carrier filters.
## FAQ
No. A message written entirely in another language, or an English message that
contains a genuine foreign word or name, is not flagged — real foreign letters
have no lookalike to impersonate. Detection keys on *disguise* (a character
pretending to be a different one), not on the mere presence of another alphabet.
No extra usage — Unicode spoofing detection is included, with no additional
model call, and adds negligible latency to a moderation request.
All of them. Detection is based on the full Unicode lookalike data, so Cyrillic,
Greek, Cherokee, Armenian, full‑width and stylized letterforms, and more are all
covered.
Yes. It's based on the official Unicode data and refreshed regularly as Unicode
publishes updates, so newly introduced lookalike characters are covered
automatically — nothing changes on your side.
# URL Risk
Source: https://docs.moderationapi.com/policies/url-risk
Real-time risk scoring for URLs in user-generated content. Catch phishing, malware, brand impersonation, and credential-harvesting links before they reach your users.
When URL Risk is on, you don't have to pass URLs separately. Anything that looks like a link in the submitted text gets pulled out and scored. Each URL goes through threat-intel feeds and a model that's seen a lot of phishing infrastructure. The response gives you a risk score and a handful of reason codes per URL.
This page documents those fields and how to interpret them.
## What counts as a link
Links are extracted whether or not the author wrote one properly:
* **Full URLs** — `https://example.com/path`
* **`www.` prefixed** — `www.example.com`
* **Markdown links** — the destination is scored, not the label
* **Bare domains** — `example.com` written mid-sentence, with no scheme and no `www.`
Bare domains are the common case for someone trying to stay under the radar, so they're checked against the public suffix list before being treated as a link. That keeps ordinary writing from being scored as a URL: filenames like `report.pdf` or `clip.mov`, missing spaces after a full stop (`I'm tired.Today I...`), bare IP addresses, and domains that only appear inside an email address are all left alone.
## Fields
```json theme={"theme":"nord"}
{
"url": "https://secure-paypal-verify.xyz/account",
"risk_score": 0.98,
"reasons": ["brand_impersonation", "suspicious_keywords", "high_risk_tld"],
"signals": {
"brand_impersonation": {
"brand": "paypal",
"method": "registered_domain_token"
},
"has_suspicious_characters": false,
"is_link_shortener": false,
"domain_age_days": null,
"has_email_setup": null,
"redirect_count": null,
"final_url": null,
"bot_protection": null,
"is_reported": false
}
}
```
| Field | Type | Meaning |
| ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url` | string | The URL that was evaluated. |
| `risk_score` | number (0.0–1.0) | Risk score. Higher is riskier. Scores at or above `0.5` are treated as malicious by default; you can apply a stricter or looser cutoff for your use case. |
| `reasons` | string\[] | Stable codes explaining *why* the URL looks risky. Empty when the URL is clean. A list of reasons means something actually flagged, not a full audit of what was checked. |
| `signals` | object | Observable properties of the URL, described below. |
### Signals
Observable properties of the URL. The shape is consistent on every request. Fields that aren't applicable or weren't checked come back as `null`.
| Field | Type | Meaning | Null when |
| --------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `brand_impersonation` | `{brand, method}` \| null | A well-known brand name appears in the URL in a way that doesn't match its legitimate domain, e.g. `paypal-verify.xyz` or `paypal.evil.com`. `brand` is the impersonated brand (e.g. `"paypal"`); `method` is `"registered_domain_token"` or `"subdomain_token"`. | No brand match detected. |
| `has_suspicious_characters` | boolean | Punycode, Unicode lookalike characters, or an unusual ratio of special characters (classic typosquatting and homograph-attack indicators). Flagged if *any* URL in the redirect chain exhibits this. | Always populated. |
| `is_link_shortener` | boolean | A free, open shortener (`bit.ly`, `tinyurl.com`, and others) is used anywhere in the redirect chain. Platform shorteners that always resolve back to their own service, like `youtu.be` or `lnkd.in`, don't count. | Always populated. |
| `domain_age_days` | integer \| null | How many days ago the destination's domain was registered. Freshly registered domains (under 30 days old) are disproportionately used for phishing. Describes the registered domain, not the subdomain. | The signal isn't informative for this URL, or wasn't needed to reach a verdict. |
| `has_email_setup` | boolean \| null | Whether the destination's domain is set up to receive email. Legitimate businesses almost always are; throwaway phishing domains often aren't. Describes the registered domain. | Not needed to reach a verdict. |
| `redirect_count` | integer \| null | Number of redirect hops from the submitted URL to its final destination. `0` means no redirect. | Not needed to reach a verdict. |
| `final_url` | string \| null | The final URL reached after following redirects. Equal to the submitted URL when there's no redirect. | Not needed to reach a verdict. |
| `bot_protection` | boolean \| null | Whether the destination sits behind a bot challenge or web application firewall. When `true`, some destination-describing signals may be `null` because we can't see past the challenge. | Not needed to reach a verdict. |
| `is_reported` | boolean | The submitted URL matches one of our threat-intelligence feeds. Stays `false` if a redirect destination is reported but the submitted URL itself isn't. | Always populated. |
Not every URL is analyzed in full depth. URLs that are clearly clean or
clearly malicious from the string alone get a fast verdict, and the
network-level signals (`domain_age_days`, `has_email_setup`, `redirect_count`,
`final_url`, `bot_protection`) come back `null`. Treat `null` as **"not
checked,"** not "not present."
#### How signals describe redirect chains
When a URL redirects across domains (e.g. a shortener resolving to a landing page), signals are assembled from both the submitted URL and the final URL:
* **Describe the destination** (where the user ends up): `brand_impersonation`, `domain_age_days`, `has_email_setup`, `bot_protection`
* **Describe the submitted URL** (what was sent): `redirect_count`, `final_url`, `is_reported`
* **Either URL exhibiting the trait**: `is_link_shortener`, `has_suspicious_characters`
Same-domain redirects (`http://` → `https://`, trailing-slash canonicalization) don't trigger re-analysis.
### Reason codes
`reasons` is an ordered list of stable codes explaining why the URL looks risky. Codes only appear when a signal or rule actually attributed risk to this URL. A field being *present* is not enough; it has to have *driven* the score. Benign URLs return `reasons: []`.
| Code | Aligns with signal | What it means |
| --------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `blocklisted` | None | The URL's hostname matched your blocklist. Verdict comes from configuration, not from analysis. |
| `allowlisted` | None | The URL's hostname matched your allowlist. Verdict comes from configuration, not from analysis. |
| `brand_impersonation` | `signals.brand_impersonation` | A brand name is used in the domain or subdomain in a way that doesn't match its legitimate home. |
| `has_suspicious_characters` | `signals.has_suspicious_characters` | Punycode, Unicode lookalikes, or an unusual special-character ratio. |
| `is_link_shortener` | `signals.is_link_shortener` | The URL uses a shortener and that pattern contributed to the risk score, or [always-flag link shorteners](#always-flagging-link-shorteners) is on. |
| `is_reported` | `signals.is_reported` | The URL is on one of our threat-intelligence feeds. |
| `new_domain` | `signals.domain_age_days` | The destination domain was registered recently and that freshness drove up risk. |
| `missing_email_setup` | `signals.has_email_setup` | The destination isn't set up for email, a common characteristic of throwaway phishing domains. |
| `high_risk_tld` | None | Top-level domain with disproportionate phishing prevalence. |
| `suspicious_keywords` | None | URL contains phishing keywords such as `login`, `verify`, `account`, `password`, `secure`. |
| `suspicious_url_structure` | None | Structural red flags: `@` in the authority, `//` redirect trick, IP address as host, URL embedded in path, credential-collecting query parameters, and similar tricks. |
| `ssl_invalid` | None | The destination's SSL certificate failed to validate. |
Reasons only describe what *increased* risk. You will not see `has_email_setup` as a reason. It's the *absence* of email setup that's concerning, and that surfaces as `missing_email_setup`.
## Allowed and blocked domains
You can override the risk model for domains you already have an opinion about. Both lists are applied before the model runs:
* A **blocklist** hit returns `risk_score: 1` and `reasons: ["blocklisted"]`.
* An **allowlist** hit returns `risk_score: 0` and `reasons: ["allowlisted"]`.
* Everything else flows through the risk model.
The verdict comes from your configuration rather than from analysis, so no `signals` are returned. The exception is a URL that was analyzed and then resolved, through a redirect, to a listed domain: the signals gathered on the way are still included.
If a domain is on both lists, the blocklist wins.
### Maintaining the lists
Each list is a [wordlist](/policies/wordlists) of domains, one domain per entry. Wordlists belong to your organization, so the same list can back several channels at once, and editing it takes effect everywhere it's used without reconfiguring anything.
To attach a list to the policy, open **URL Risk** on the channel and use the **Allowed domains** or **Blocked domains** tab. Every wordlist in your organization is listed there; toggle on the ones this channel should use. You can also create a new wordlist straight from that tab if you don't have one yet.
To add or edit domains, open the wordlist itself in [Model Studio](https://dash.moderationapi.com/models) under **Wordlists**. Type entries in directly or upload a CSV or spreadsheet. The tab in the policy also links to each wordlist for quick edits.
Domain matching is exact. The wordlist's flagging threshold and semantic
matching don't apply here, so a list used for URL Risk won't match near-misses
or related words the way a wordlist does on text.
### How entries match
Entries match on the **full hostname**, with `www.` normalized away and case ignored. Subdomains are *not* matched automatically. To allow every subdomain of your service, add each one explicitly.
Given an allowlist entry of `example.com`:
| URL | Matches? |
| -------------------------------- | ------------------------------------------ |
| `https://example.com/page` | Yes |
| `https://www.example.com/page` | Yes (`www` is normalized away) |
| `https://login.example.com/page` | No, add `login.example.com` explicitly |
| `https://api.prod.example.com/` | No, add `api.prod.example.com` explicitly |
| `https://example.com.evil.xyz/` | No, the hostname is `example.com.evil.xyz` |
Enter plain domains without wildcards. If you paste a full URL, the scheme and path are stripped for you.
## Always flagging link shorteners
Shortened links hide their destination, and some platforms would rather not carry them at all. **Always flag free link shorteners**, on the URL Risk policy, turns that into a hard rule: any URL using a shortener comes back with `risk_score: 1` and `is_link_shortener` in `reasons`, whatever the model made of the destination. `signals` are still returned, so you can see what the destination looked like.
The setting covers the free, open shorteners anyone can create a link with (`bit.ly`, `tinyurl.com`, and others). Platform shorteners that can only ever point back at their own service, like `youtu.be` or `lnkd.in`, aren't treated as shorteners here, so turning this on won't flag every YouTube link your users post.
Allowlisted domains still pass. If you want to keep one shortener you rely on, add its domain to an allowlist and the setting won't touch it.
## FAQ
Risk is a moving target. Several inputs change between requests:
* **Domains age.** A freshly registered domain looks risky today and less risky in six months. `domain_age_days` grows naturally.
* **Email infrastructure gets added.** Legitimate businesses set up MX, SPF, and DMARC records as they grow up; throwaway domains rarely do. `has_email_setup` can flip from `false` to `true` as a domain matures.
* **Threat-intelligence feeds update constantly.** A URL not on any feed today may be reported tomorrow.
* **Redirect destinations change.** Shorteners and redirectors can be repointed at any time. The destination is re-resolved on every request.
* **The model is updated** as the threat landscape shifts.
If you're caching scores, cache them briefly. Re-evaluate any URL still in active circulation rather than relying on a result that's hours or days old.
`risk_score >= 0.5` is the default cutoff for "treat as malicious," and it's tuned so the rate of false positives at that threshold is low across typical user-generated content. Tighten it (e.g. `0.7`) if your audience is unusually tolerant of risky links, or loosen it (e.g. `0.3`) if you'd rather over-block. The `reasons` array gives you the *why* in either direction.
Add its domain to an [allowlist](#maintaining-the-lists). Allowlist entries override the risk model, which makes them the right tool for your own product domains, trusted partners, and URLs you've manually verified as safe.
If you think the score is wrong in a way that would also affect other customers (for example, a brand-impersonation false positive on a legitimate brand variant), let us know and we'll look at the model.
# Wordlists
Source: https://docs.moderationapi.com/policies/wordlists
Flag, require, or extract your own words and phrases with wordlists that understand semantic meaning, typos, and variations.
Wordlists let you moderate against your own list of words and phrases — brand names, competitor mentions, or terms specific to your community. Unlike a plain string match, wordlists understand semantic meaning: add `YouTube` and semantically similar words like `Vimeo` can be flagged too; add the phrase `New York` and it also matches `NYC`. Tense, plural forms, and slight variations are covered without you having to add every spelling yourself.
Wordlists belong to your organization and are enabled per channel, so the same list can back several channels at once — editing it takes effect everywhere it's used.
## When to use a wordlist
Use a wordlist when the rule is about specific words or phrases: a list of brands, competitor names, or terms that should never (or always) appear.
When the rule is about context and intent rather than particular words, [Guidelines](/policies/guidelines) are usually the better choice — they evaluate what the content means, not which words it uses.
## Creating a wordlist
Create wordlists in [Model Studio](https://dash.moderationapi.com/models) under **Wordlists**, or directly from your project. Add entries by typing them in or by uploading a CSV or Excel file — each column in a spreadsheet is treated as a separate entry.
Entries follow a few rules:
* **Case insensitive** — entries are automatically converted to lowercase.
* **Deduplicated** — adding `apple` twice keeps a single entry.
* **Phrases work too** — a phrase is matched as you typed it, but with the same semantic understanding as single words.
* **Large uploads process in the background** — adding many entries at once triggers processing that can take a few minutes, and the wordlist won't detect words until it completes.
You can also manage entries programmatically with the [wordlist API](/api-reference/wordlist/list-wordlists).
## Flagging threshold
Because wordlists understand semantic meaning, you choose how similar a word needs to be before it's flagged. Thresholds are set per channel — the same wordlist can be linked to multiple channels with a different threshold on each, so you can be stricter in some places than others.
There are 4 presets, plus a custom option where you can enter any percentage between 0 and 100:
* **Exact match (100%)**: Only flag matches that are exactly the same as the words or phrases in the wordlist.
* **Same word (90%)**: Also flag matches that contain typos or slight variations, but are otherwise the same word.
* **Similar word (>50%)**: Also flag matches that are semantically similar to the words in the wordlist.
* **Nearest word (>10%)**: Always return matches, even if they are not semantically similar. Useful for debugging or finding the closest word in a text.
## Flagging mode
Like the threshold, the mode is set per channel:
| Mode | Behavior | Use it for |
| ---------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| **Block list** | A match flags the content. | Preventing certain words from being used on your platform. This is the default. |
| **Require list** | Content is flagged when it does **not** contain a match. | Requiring certain words to be present, e.g. on-topic keywords. |
| **Pass** | Never flags, but matches are still returned and [masking](/content-moderation/acting-on-responses) still applies. | Collecting match data for analysis, or masking specific entities without flagging. |
## In API responses
Each wordlist enabled on a channel returns its results inside the `policies` array of the [moderation response](/content-moderation/acting-on-responses). The policy `type` is `entity_matcher` and the `id` is `wordlist/` — the key you set when creating the wordlist.
| Field | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `wordlist/` — identifies which wordlist this result is from |
| `type` | Always `entity_matcher` for wordlists |
| `flagged` | Whether this wordlist caused the content to be flagged. Depends on the wordlist's mode (block / require / pass) |
| `flagged_fields` | For object submissions, which fields contained matches that triggered the flag |
| `matches[]` | The matched words. Each entry has `match` (the word), `probability` (similarity score 0–1), `span` (`[start, end]`), and `mask` |
```json Response example theme={"theme":"nord"}
{
"policies": [
{
"id": "wordlist/brandlist",
"type": "entity_matcher",
"flagged": true,
"flagged_fields": [],
"matches": [
{
"match": "youtube",
"probability": 0.9999997615814209,
"span": [12, 19],
"mask": null
}
]
},
{
"id": "wordlist/test",
"type": "entity_matcher",
"flagged": false,
"flagged_fields": [],
"matches": []
}
]
}
```
Read a specific wordlist result by its key:
```javascript theme={"theme":"nord"}
const brandlist = response.policies.find(p => p.id === "wordlist/brandlist");
if (brandlist?.flagged) {
brandlist.matches.forEach(m => console.log(`${m.match} at ${m.span}`));
}
```
## Debugging wordlists
If you wonder why a word is or isn't flagged, lower the flagging threshold to see the similarity score of the nearest match — this shows how similar the word is perceived to be. The project playground is a good way to quickly test and debug a wordlist.
**Legacy wordlist model** — we previously offered wordlists as a pre-built
model. That model matched words literally without semantic understanding. If
you still use it, we recommend switching to the wordlists feature described
here — it's smarter and more flexible.
# Content moderation with .NET
Source: https://docs.moderationapi.com/quickstart/dotnet
Learn how to moderate content using Moderation API's REST endpoints with .NET
***
***
## 1. Install dependencies
We currently don't have an official SDK for .NET, but you can use the [OpenAPI Generator](/resources/generate-client-openapi) to generate a .NET client or simply call the API directly using HttpClient.
```bash .NET CLI theme={"theme":"nord"}
dotnet add package System.Text.Json
```
```bash Package Manager theme={"theme":"nord"}
Install-Package System.Text.Json
```
## 2. Submit content
Grab the API key from your [project](https://dash.moderationapi.com) and begin submitting text, images, or other media to your project for moderation.
```csharp Text moderation theme={"theme":"nord"}
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class ModerationClient
{
private readonly HttpClient _httpClient;
private const string BaseUrl = "https://api.moderationapi.com/v1";
public ModerationClient(string apiKey)
{
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
}
public async Task ModerateTextAsync(string text, string authorId = "123", string conversationId = "456")
{
var payload = new
{
content = new
{
type = "text",
text = text
},
authorId = authorId,
conversationId = conversationId,
metadata = new { customField = "value" }
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{BaseUrl}/moderate", content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize(responseContent);
}
}
public class TextModerationResult
{
public bool flagged { get; set; }
public object[] categories { get; set; }
}
// Usage example
class Program
{
static async Task Main(string[] args)
{
var client = new ModerationClient("your-api-key"); // Replace with your API key
var result = await client.ModerateTextAsync("Hello world!");
if (result.flagged)
{
Console.WriteLine("Text content flagged");
// Block the content, show an error, etc...
}
else
{
Console.WriteLine("Text content is safe.");
// Save to database or proceed...
}
}
}
```
```csharp Image moderation theme={"theme":"nord"}
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class ModerationClient
{
private readonly HttpClient _httpClient;
private const string BaseUrl = "https://api.moderationapi.com/v1";
public ModerationClient(string apiKey)
{
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
}
public async Task ModerateImageAsync(string imageUrl, string authorId = "123", string conversationId = "456")
{
var payload = new
{
content = new
{
type = "image",
url = imageUrl
},
authorId = authorId,
conversationId = conversationId,
metadata = new { customField = "value" }
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{BaseUrl}/moderate", content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize(responseContent);
}
}
public class ImageModerationResult
{
public bool flagged { get; set; }
public string[] labels { get; set; }
}
// Usage example
class Program
{
static async Task Main(string[] args)
{
var client = new ModerationClient("your-api-key"); // Replace with your API key
var result = await client.ModerateImageAsync("https://example.com/image.jpg");
if (result.flagged)
{
Console.WriteLine($"Image content flagged: {string.Join(", ", result.labels)}");
// Block or require review
}
else
{
Console.WriteLine("Image is safe.");
// Save to database or proceed...
}
}
}
```
```csharp Object moderation theme={"theme":"nord"}
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class ModerationClient
{
private readonly HttpClient _httpClient;
private const string BaseUrl = "https://api.moderationapi.com/v1";
public ModerationClient(string apiKey)
{
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
}
public async Task ModerateObjectAsync(object objectData, string authorId = "123", string conversationId = "456")
{
var payload = new
{
content = new
{
type = "object",
data = new
{
name = new { type = "text", text = "John Doe" },
email = new { type = "text", text = "john.doe@example.com" },
picture = new { type = "image", url = "https://example.com/image.jpg" }
}
},
authorId = authorId,
conversationId = conversationId,
metadata = new { customField = "value" }
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{BaseUrl}/moderate", content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize(responseContent);
}
}
public class ObjectModerationResult
{
public bool flagged { get; set; }
}
// Usage example
class Program
{
static async Task Main(string[] args)
{
var client = new ModerationClient("your-api-key"); // Replace with your API key
var result = await client.ModerateObjectAsync(new { });
if (result.flagged)
{
Console.WriteLine("Object content flagged");
// Block or require review
}
else
{
Console.WriteLine("Object is safe.");
// Save to database or proceed...
}
}
}
```
***
***
***
# Content moderation with Go
Source: https://docs.moderationapi.com/quickstart/go
Learn how to moderate content using Moderation API's REST endpoints with Go
***
***
## 1. Install SDK
We currently don't have an official SDK for Go, but you can use the [OpenAPI Generator](/resources/generate-client-openapi) to generate a Go client or simply call the API directly using Go's standard `net/http` package.
## 2. Submit content
Grab the API key from your [project](https://dash.moderationapi.com) and begin submitting text, images, or other media to your project for moderation.
```go Text moderation theme={"theme":"nord"}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// Configure the client
apiKey := "your-api-key" // Replace with your API key
baseURL := "https://api.moderationapi.com/v1"
// Prepare request body
requestBody, err := json.Marshal(map[string]interface{}{
"content": map[string]interface{}{
"type": "text",
"text": "Hello world!",
},
"authorId": "user-123",
"conversationId": "room-456",
"metadata": map[string]interface{}{
"customField": "value",
},
})
if err != nil {
panic(err)
}
// Create request
req, err := http.NewRequest("POST", baseURL+"/moderate", bytes.NewBuffer(requestBody))
if err != nil {
panic(err)
}
// Set headers
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
// Send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Read response
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
// Parse response
var textAnalysis map[string]interface{}
if err := json.Unmarshal(body, &textAnalysis); err != nil {
panic(err)
}
evaluation := textAnalysis["evaluation"].(map[string]interface{})
if evaluation["flagged"].(bool) {
fmt.Println("Text content flagged")
// Block the content, show an error, etc...
} else {
fmt.Println("Text content is safe.")
// Save to database or proceed...
}
}
```
```go Image moderation theme={"theme":"nord"}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// Configure the client
apiKey := "your-api-key" // Replace with your API key
baseURL := "https://api.moderationapi.com/v1"
// Prepare request body
requestBody, err := json.Marshal(map[string]interface{}{
"content": map[string]interface{}{
"type": "image",
"url": "https://example.com/image.jpg",
},
"authorId": "user-123",
"metadata": map[string]interface{}{
"customField": "value",
},
})
if err != nil {
panic(err)
}
// Create request
req, err := http.NewRequest("POST", baseURL+"/moderate", bytes.NewBuffer(requestBody))
if err != nil {
panic(err)
}
// Set headers
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
// Send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Read response
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
// Parse response
var imageAnalysis map[string]interface{}
if err := json.Unmarshal(body, &imageAnalysis); err != nil {
panic(err)
}
evaluation := imageAnalysis["evaluation"].(map[string]interface{})
if evaluation["flagged"].(bool) {
fmt.Println("Image content flagged")
// Block or require review
} else {
fmt.Println("Image is safe.")
// Save to database or proceed...
}
}
```
```go Object moderation theme={"theme":"nord"}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// Configure the client
apiKey := "your-api-key" // Replace with your API key
baseURL := "https://api.moderationapi.com/v1"
// Prepare request body
requestBody, err := json.Marshal(map[string]interface{}{
"content": map[string]interface{}{
"type": "object",
"data": map[string]interface{}{
"name": map[string]interface{}{
"type": "text",
"text": "John Doe",
},
"email": map[string]interface{}{
"type": "text",
"text": "john.doe@example.com",
},
"picture": map[string]interface{}{
"type": "image",
"url": "https://example.com/image.jpg",
},
},
},
"authorId": "user-123",
"metadata": map[string]interface{}{
"customField": "value",
},
})
if err != nil {
panic(err)
}
// Create request
req, err := http.NewRequest("POST", baseURL+"/moderate", bytes.NewBuffer(requestBody))
if err != nil {
panic(err)
}
// Set headers
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
// Send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Read response
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
// Parse response
var objectAnalysis map[string]interface{}
if err := json.Unmarshal(body, &objectAnalysis); err != nil {
panic(err)
}
evaluation := objectAnalysis["evaluation"].(map[string]interface{})
if evaluation["flagged"].(bool) {
fmt.Println("Object content flagged")
// Block or require review
} else {
fmt.Println("Object is safe.")
// Save to database or proceed...
}
}
```
***
***
***
# Content moderation with Java
Source: https://docs.moderationapi.com/quickstart/java
Learn how to moderate content using Moderation API's REST endpoints with Java
***
***
## 1. Install SDK
We currently don't have an official SDK for Java, but you can use the [OpenAPI Generator](/resources/generate-client-openapi) to generate a Java client or simply call the API directly using Java's built-in `HttpClient`.
## 2. Submit content
Grab the API key from your [project](https://dash.moderationapi.com) and begin submitting text, images, or other media to your project for moderation.
```java Text moderation theme={"theme":"nord"}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.json.JSONObject;
public class TextModeration {
public static void main(String[] args) {
String API_KEY = "your-api-key"; // Replace with your API key
String BASE_URL = "https://api.moderationapi.com/v1";
try {
HttpClient client = HttpClient.newHttpClient();
// Create request body
JSONObject content = new JSONObject()
.put("type", "text")
.put("text", "Hello world!");
JSONObject requestBody = new JSONObject()
.put("content", content)
.put("authorId", "123")
.put("conversationId", "456")
.put("metadata", new JSONObject()
.put("customField", "value")
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/moderate"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody.toString()))
.build();
HttpResponse response = client.send(request,
HttpResponse.BodyHandlers.ofString());
JSONObject textAnalysis = new JSONObject(response.body());
if (textAnalysis.getBoolean("flagged")) {
System.out.println("Text content flagged");
// Block the content, show an error, etc...
} else {
System.out.println("Text content is safe.");
// Save to database or proceed...
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
```java Image moderation theme={"theme":"nord"}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.json.JSONObject;
public class ImageModeration {
public static void main(String[] args) {
String API_KEY = "your-api-key"; // Replace with your API key
String BASE_URL = "https://api.moderationapi.com/v1";
try {
HttpClient client = HttpClient.newHttpClient();
// Create request body
JSONObject content = new JSONObject()
.put("type", "image")
.put("url", "https://example.com/image.jpg");
JSONObject requestBody = new JSONObject()
.put("content", content)
.put("authorId", "123")
.put("conversationId", "456")
.put("metadata", new JSONObject()
.put("customField", "value")
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/moderate"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody.toString()))
.build();
HttpResponse response = client.send(request,
HttpResponse.BodyHandlers.ofString());
JSONObject imageAnalysis = new JSONObject(response.body());
if (imageAnalysis.getBoolean("flagged")) {
System.out.println("Image content flagged: " +
imageAnalysis.getJSONArray("labels"));
// Block or require review
} else {
System.out.println("Image is safe.");
// Save to database or proceed...
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
```java Object moderation theme={"theme":"nord"}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.json.JSONObject;
public class ObjectModeration {
public static void main(String[] args) {
String API_KEY = "your-api-key"; // Replace with your API key
String BASE_URL = "https://api.moderationapi.com/v1";
try {
HttpClient client = HttpClient.newHttpClient();
// Create request body
JSONObject profileData = new JSONObject()
.put("name", new JSONObject()
.put("type", "text")
.put("text", "John Doe"))
.put("email", new JSONObject()
.put("type", "text")
.put("text", "john.doe@example.com"))
.put("picture", new JSONObject()
.put("type", "image")
.put("url", "https://example.com/image.jpg"));
JSONObject content = new JSONObject()
.put("type", "object")
.put("data", profileData);
JSONObject requestBody = new JSONObject()
.put("content", content)
.put("authorId", "123")
.put("conversationId", "456")
.put("metadata", new JSONObject()
.put("customField", "value"));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/moderate"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody.toString()))
.build();
HttpResponse response = client.send(request,
HttpResponse.BodyHandlers.ofString());
JSONObject objectAnalysis = new JSONObject(response.body());
if (objectAnalysis.getBoolean("flagged")) {
System.out.println("Object content flagged");
// Block or require review
} else {
System.out.println("Object is safe.");
// Save to database or proceed...
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
***
***
***
# Content moderation with PHP
Source: https://docs.moderationapi.com/quickstart/php
Learn how to moderate content using Moderation API's PHP SDK
***
***
## 1. Install the SDK
Install the official PHP SDK via Composer:
```bash theme={"theme":"nord"}
composer require moderation-api/sdk-php
```
Requires PHP 8.1.0 or higher. The SDK is currently in beta.
## 2. Submit content
Grab the API key from your [project](https://dash.moderationapi.com) and begin submitting text, images, or other media to your project for moderation.
```php Text moderation theme={"theme":"nord"}
content->submit([
'content' => [
'type' => 'text',
'text' => 'Hello world!'
],
'authorId' => '123',
'conversationId' => '456',
'metadata' => [
'customField' => 'value'
]
]);
// Check if content was flagged
if ($response->evaluation->flagged) {
echo "Content was flagged\n";
}
// Act on the recommendation
switch ($response->recommendation->action) {
case 'allow':
echo "Text content is safe.\n";
// Save to database or proceed...
break;
case 'review':
echo "Text content needs review\n";
// Send to review queue
break;
case 'reject':
echo "Text content rejected\n";
// Block the content, show an error, etc...
break;
}
```
```php Image moderation theme={"theme":"nord"}
content->submit([
'content' => [
'type' => 'image',
'url' => 'https://example.com/image.jpg'
],
'authorId' => '123',
'conversationId' => '456',
'metadata' => [
'customField' => 'value'
]
]);
// Check if content was flagged
if ($response->evaluation->flagged) {
echo "Image was flagged\n";
}
// Act on the recommendation
switch ($response->recommendation->action) {
case 'allow':
echo "Image is safe.\n";
// Save to database or proceed...
break;
case 'review':
echo "Image needs review\n";
// Send to review queue
break;
case 'reject':
echo "Image rejected\n";
// Block or require review
break;
}
```
```php Object moderation theme={"theme":"nord"}
content->submit([
'content' => [
'type' => 'object',
'data' => [
'name' => [
'type' => 'text',
'text' => 'John Doe'
],
'email' => [
'type' => 'text',
'text' => 'john.doe@example.com'
],
'picture' => [
'type' => 'image',
'url' => 'https://example.com/image.jpg'
]
]
],
'authorId' => '123',
'conversationId' => '456',
'metadata' => [
'customField' => 'value'
]
]);
// Check if content was flagged
if ($response->evaluation->flagged) {
echo "Object was flagged\n";
}
// Act on the recommendation
switch ($response->recommendation->action) {
case 'allow':
echo "Object is safe.\n";
// Save to database or proceed...
break;
case 'review':
echo "Object needs review\n";
// Send to review queue
break;
case 'reject':
echo "Object rejected\n";
// Block or require review
break;
}
```
## 3. Handle errors
The SDK throws specific exception types for different error scenarios:
```php theme={"theme":"nord"}
content->submit([
'content' => [
'type' => 'text',
'text' => 'Hello world!'
]
]);
echo "Recommendation: " . $response->recommendation->action . "\n";
} catch (BadRequestException $e) {
echo "Invalid request: " . $e->getMessage();
} catch (AuthenticationException $e) {
echo "Authentication failed: " . $e->getMessage();
} catch (RateLimitException $e) {
echo "Rate limit exceeded: " . $e->getMessage();
} catch (APIConnectionException $e) {
echo "Connection error: " . $e->getMessage();
}
```
The SDK automatically retries failed requests up to 2 times by default. You can configure retries globally when initializing the client with `maxRetries: 0` or per-request using `RequestOptions::with(maxRetries: 5)`.
***
***
***
# Content moderation with Python
Source: https://docs.moderationapi.com/quickstart/python
Learn how to moderate content using Moderation API's Python SDK
***
***
## 1. Install the SDK
```bash theme={"theme":"nord"}
pip install moderation_api
```
Requires Python 3.9 or higher. For improved async performance, install with `pip install moderation_api[aiohttp]`.
## 2. Submit content
Grab the API key from your [project](https://dash.moderationapi.com) and begin submitting text, images, or other media to your project for moderation.
```python Text moderation theme={"theme":"nord"}
import os
from moderation_api import ModerationAPI
# Initialize the client with your secret key
client = ModerationAPI(
secret_key=os.environ.get("MODAPI_SECRET_KEY")
)
# Submit text for moderation
response = client.content.submit(
content={
"type": "text",
"text": "Hello world!"
},
author_id="123",
conversation_id="456",
metadata={
"custom_field": "value"
}
)
# Check if content was flagged
if response.evaluation.flagged:
print("Content was flagged")
# Act on the recommendation
if response.recommendation.action == "allow":
print("Text content is safe.")
# Save to database or proceed...
elif response.recommendation.action == "review":
print("Text content needs review")
# Send to review queue
elif response.recommendation.action == "reject":
print("Text content rejected")
# Block the content, show an error, etc...
```
```python Image moderation theme={"theme":"nord"}
import os
from moderation_api import ModerationAPI
# Initialize the client with your secret key
client = ModerationAPI(
secret_key=os.environ.get("MODAPI_SECRET_KEY")
)
# Submit image for moderation
response = client.content.submit(
content={
"type": "image",
"url": "https://example.com/image.jpg"
},
author_id="123",
conversation_id="456",
metadata={
"custom_field": "value"
}
)
# Check if content was flagged
if response.evaluation.flagged:
print("Image was flagged")
# Act on the recommendation
if response.recommendation.action == "allow":
print("Image is safe.")
# Save to database or proceed...
elif response.recommendation.action == "review":
print("Image needs review")
# Send to review queue
elif response.recommendation.action == "reject":
print("Image rejected")
# Block or require review
```
```python Object moderation theme={"theme":"nord"}
import os
from moderation_api import ModerationAPI
# Initialize the client with your secret key
client = ModerationAPI(
secret_key=os.environ.get("MODAPI_SECRET_KEY")
)
# Submit object for moderation
response = client.content.submit(
content={
"type": "object",
"data": {
"name": {
"type": "text",
"text": "John Doe"
},
"email": {
"type": "text",
"text": "john.doe@example.com"
},
"picture": {
"type": "image",
"url": "https://example.com/image.jpg"
}
}
},
author_id="123",
conversation_id="456",
metadata={
"custom_field": "value"
}
)
# Check if content was flagged
if response.evaluation.flagged:
print("Object was flagged")
# Act on the recommendation
if response.recommendation.action == "allow":
print("Object is safe.")
# Save to database or proceed...
elif response.recommendation.action == "review":
print("Object needs review")
# Send to review queue
elif response.recommendation.action == "reject":
print("Object rejected")
# Block or require review
```
## 3. Handle errors
The SDK raises specific exception types for different error scenarios:
```python theme={"theme":"nord"}
import os
from moderation_api import ModerationAPI
from moderation_api import APIConnectionError, APIStatusError
client = ModerationAPI(
secret_key=os.environ.get("MODAPI_SECRET_KEY")
)
try:
response = client.content.submit(
content={
"type": "text",
"text": "Hello world!"
}
)
print(f"Recommendation: {response.recommendation.action}")
except APIStatusError as e:
print(f"API error {e.status_code}: {e.message}")
except APIConnectionError as e:
print(f"Connection error: {e}")
```
The SDK automatically retries failed requests up to 2 times by default. You can configure retries when initializing the client with `max_retries=0` or per-request.
***
***
***
# Content moderation with Ruby
Source: https://docs.moderationapi.com/quickstart/ruby
Learn how to moderate content using Moderation API's Ruby SDK
***
***
## 1. Install the SDK
```bash gem theme={"theme":"nord"}
gem install moderation_api
```
```bash bundler theme={"theme":"nord"}
# Add this to your Gemfile
gem 'moderation_api', '~> 2.0.0'
# Then run
bundle install
```
Requires Ruby 3.2.0 or higher.
## 2. Submit content
Grab the API key from your [project](https://dash.moderationapi.com) and begin submitting text, images, or other media to your project for moderation.
```ruby Text moderation theme={"theme":"nord"}
require "moderation_api"
# Initialize the client with your secret key
moderation_api = ModerationAPI::Client.new(
secret_key: ENV["MODAPI_SECRET_KEY"] || "your-api-key"
)
# Submit text for moderation
response = moderation_api.content.submit(
content: {
type: "text",
text: "Hello world!"
},
author_id: "123",
conversation_id: "456",
metadata: {
custom_field: "value"
}
)
# Check if content was flagged
if response.evaluation.flagged
puts "Content was flagged"
end
# Act on the recommendation
case response.recommendation.action
when :allow
puts "Text content is safe."
# Save to database or proceed...
when :review
puts "Text content needs review"
# Send to review queue
when :reject
puts "Text content rejected"
# Block the content, show an error, etc...
end
```
```ruby Image moderation theme={"theme":"nord"}
require "moderation_api"
# Initialize the client with your secret key
moderation_api = ModerationAPI::Client.new(
secret_key: ENV["MODAPI_SECRET_KEY"] || "your-api-key"
)
# Submit image for moderation
response = moderation_api.content.submit(
content: {
type: "image",
url: "https://example.com/image.jpg"
},
author_id: "123",
conversation_id: "456",
metadata: {
custom_field: "value"
}
)
# Check if content was flagged
if response.evaluation.flagged
puts "Image was flagged"
end
# Act on the recommendation
case response.recommendation.action
when :allow
puts "Image is safe."
# Save to database or proceed...
when :review
puts "Image needs review"
# Send to review queue
when :reject
puts "Image rejected"
# Block or require review
end
```
```ruby Object moderation theme={"theme":"nord"}
require "moderation_api"
# Initialize the client with your secret key
moderation_api = ModerationAPI::Client.new(
secret_key: ENV["MODAPI_SECRET_KEY"] || "your-api-key"
)
# Submit object for moderation
response = moderation_api.content.submit(
content: {
type: "object",
data: {
name: {
type: "text",
text: "John Doe"
},
email: {
type: "text",
text: "john.doe@example.com"
},
picture: {
type: "image",
url: "https://example.com/image.jpg"
}
}
},
author_id: "123",
conversation_id: "456",
metadata: {
custom_field: "value"
}
)
# Check if content was flagged
if response.evaluation.flagged
puts "Object was flagged"
end
# Act on the recommendation
case response.recommendation.action
when :allow
puts "Object is safe."
# Save to database or proceed...
when :review
puts "Object needs review"
# Send to review queue
when :reject
puts "Object rejected"
# Block or require review
end
```
## 3. Handle errors
The SDK throws specific error types for different scenarios:
```ruby theme={"theme":"nord"}
require "moderation_api"
moderation_api = ModerationAPI::Client.new(
secret_key: ENV["MODAPI_SECRET_KEY"] || "your-api-key"
)
begin
response = moderation_api.content.submit(
content: {
type: "text",
text: "Hello world!"
}
)
puts "Recommendation: #{response.recommendation.action}"
rescue ModerationAPI::Errors::BadRequestError => e
puts "Invalid request: #{e.message}"
rescue ModerationAPI::Errors::AuthenticationError => e
puts "Authentication failed: #{e.message}"
rescue ModerationAPI::Errors::RateLimitError => e
puts "Rate limit exceeded: #{e.message}"
rescue ModerationAPI::Errors::APIConnectionError => e
puts "Connection error: #{e.message}"
rescue ModerationAPI::Errors::APITimeoutError => e
puts "Request timed out: #{e.message}"
rescue ModerationAPI::Errors::InternalServerError => e
puts "Server error: #{e.message}"
end
```
The SDK automatically retries failed requests up to 2 times by default. Configure retries globally with `max_retries: 0` when initializing the client, or per-request using `request_options: {max_retries: 5}`. Timeouts default to 60 seconds and can be customized similarly.
***
***
***
# Content moderation with TypeScript
Source: https://docs.moderationapi.com/quickstart/typescript
Learn how to moderate content using Moderation API's TypeScript SDK
***
***
## 1. Install the SDK
```bash npm theme={"theme":"nord"}
npm install @moderation-api/sdk
```
```bash yarn theme={"theme":"nord"}
yarn add @moderation-api/sdk
```
```bash pnpm theme={"theme":"nord"}
pnpm add @moderation-api/sdk
```
## 2. Submit content
Grab the API key from your [project](https://dash.moderationapi.com) and begin submitting text, images, or other media to your project for moderation.
```typescript Text moderation theme={"theme":"nord"}
import ModerationAPI from "@moderation-api/sdk";
// Option 1: Use environment variable MODAPI_SECRET_KEY
const moderationApi = new ModerationAPI();
// Option 2: Pass key explicitly
// const moderationApi = new ModerationAPI({ secretKey: 'proj_...' });
// Submit text for moderation
const result = await moderationApi.content.submit({
content: {
type: "text",
text: "Hello world!",
},
contentId: "message-123",
authorId: "user-123",
conversationId: "room-456",
metadata: {
customField: "value",
},
});
// Simple boolean check
if (result.evaluation.flagged) {
console.warn("Text content flagged");
// Block the content, show an error, etc...
}
// Use the API's recommendation (considers severity, thresholds, and more)
switch (result.recommendation.action) {
case "reject":
console.log("Content should be rejected");
break;
case "review":
console.log("Content needs manual review");
break;
case "allow":
console.log("Content is approved");
break;
}
```
```typescript Image moderation theme={"theme":"nord"}
import ModerationAPI from "@moderation-api/sdk";
// Configure your client using environment variable MODAPI_SECRET_KEY
const moderationApi = new ModerationAPI();
// Submit image for moderation
const result = await moderationApi.content.submit({
content: {
type: "image",
url: "https://example.com/image.jpg",
},
contentId: "image-456",
authorId: "user-123",
metadata: {
customField: "value",
},
});
// Check if content was flagged
if (result.evaluation.flagged) {
console.warn("Image content flagged");
// Block or require review
}
// Use the API's recommendation
if (result.recommendation.action === "reject") {
console.log("Image should be blocked");
} else if (result.recommendation.action === "review") {
console.log("Image needs manual review");
} else {
console.log("Image is approved");
}
```
```typescript Object moderation theme={"theme":"nord"}
import ModerationAPI from "@moderation-api/sdk";
// Configure your client using environment variable MODAPI_SECRET_KEY
const moderationApi = new ModerationAPI();
// Submit object (complex data) for moderation
const result = await moderationApi.content.submit({
content: {
type: "object",
data: {
title: { type: "text", text: "Post title" },
body: { type: "text", text: "Post content" },
thumbnail: { type: "image", url: "https://example.com/thumb.jpg" },
},
},
contentId: "post-789",
authorId: "user-123",
metadata: {
customField: "value",
},
});
// Check if content was flagged
if (result.evaluation.flagged) {
console.warn("Object content flagged");
// Block or require review
}
// Use the API's recommendation
switch (result.recommendation.action) {
case "reject":
console.log("Object should be rejected");
break;
case "review":
console.log("Object needs manual review");
break;
case "allow":
console.log("Object is approved");
break;
}
```
***
***
***
# Generate Client Libraries
Source: https://docs.moderationapi.com/resources/generate-client-openapi
Learn how to generate client libraries with our OpenAPI specification
## Overview
OpenAPI (formerly known as Swagger) is a widely adopted framework for describing RESTful APIs. By leveraging our OpenAPI 3.0.3 specification, you can automatically generate client libraries in many different programming languages. This ensures that you can easily integrate with our API, reduce boilerplate code, and minimize the risk of coding mistakes.
## Getting the OpenAPI Specification
You can find our OpenAPI specification at the following URL:
```bash theme={"theme":"nord"}
https://api.moderationapi.com/v1/openapi.json
```
Feel free to download the specification file or use the URL directly with any OpenAPI-compatible tool.
***
## Using OpenAPI Generator
[OpenAPI Generator](https://openapi-generator.tech/) is a widely used command-line tool for generating client libraries from OpenAPI specifications.
### 1. Install OpenAPI Generator CLI
Below are several ways to install the CLI on your system:
```bash theme={"theme":"nord"}
# Using npm
npm install @openapitools/openapi-generator-cli -g
# Using Homebrew (macOS)
brew install openapi-generator
# Using Docker
docker pull openapitools/openapi-generator-cli
```
### 2. Generate a Client Library
With the CLI installed, you can generate a client library by specifying the input specification URL (`-i`) and the target language or framework (`-g`). Here are some examples:
```bash theme={"theme":"nord"}
# Python
openapi-generator generate \
-i https://api.moderationapi.com/v1/openapi.json \
-g python \
-o ./client-python
# TypeScript (Axios)
openapi-generator generate \
-i https://api.moderationapi.com/v1/openapi.json \
-g typescript-axios \
-o ./client-typescript
# Java
openapi-generator generate \
-i https://api.moderationapi.com/v1/openapi.json \
-g java \
-o ./client-java
```
OpenAPI Generator supports [over 50 languages](https://openapi-generator.tech/docs/generators) and frameworks—feel free to explore additional options.
***
## Using Swagger Codegen
[Swagger Codegen](https://github.com/swagger-api/swagger-codegen) is an alternative to OpenAPI Generator and also supports a range of languages.
### 1. Install Swagger Codegen
```bash theme={"theme":"nord"}
# Using Homebrew (macOS)
brew install swagger-codegen
# Using Docker
docker pull swaggerapi/swagger-codegen-cli
```
### 2. Generate a Client
You can generate a client similarly by specifying the specification URL or a local file:
```bash theme={"theme":"nord"}
swagger-codegen generate \
-i https://api.moderationapi.com/v1/openapi.json \
-l python \
-o ./client-python
```
***
## Language-Specific Tools
Depending on your language of choice, you may want to use specialized tools that simplify the process even further.
### Python
If you prefer a Python-first approach, consider [openapi-python-client](https://github.com/openapi-generators/openapi-python-client). It can generate Python clients that align well with modern Pythonic standards:
```bash theme={"theme":"nord"}
pip install openapi-python-client
openapi-python-client generate \
--url https://api.moderationapi.com/v1/openapi.json
```
By default, this creates a new folder with a Python package ready to install and use.
### TypeScript
For projects that primarily use TypeScript, [openapi-typescript](https://github.com/drwpow/openapi-typescript) provides a type-safe way to consume the specification:
```bash theme={"theme":"nord"}
npm install openapi-typescript
npx openapi-typescript https://api.moderationapi.com/v1/openapi.json -o types.ts
```
This approach focuses on generating TypeScript type definitions rather than a fully featured client library, enabling you to write custom service logic while still benefiting from type safety.
***
## Verifying Your Generated Client
1. Install the generated client's dependencies (e.g., run npm install for Node.js or pip install . for Python) according to its README instructions.
2. Initialize the client with your API key or Bearer token (depending on your authentication mechanism).
3. Make a small test request to ensure the client communicates with the API successfully.
***
## Common Tips and Troubleshooting
* Make sure your OpenAPI specification is valid. You can use the [OpenAPI Validator](https://validator.swagger.io/) to check for any syntax or schema issues.
* Keep your generated client code up-to-date with the latest version of the specification by regenerating when the API changes.
* For language-specific best practices (like environment configuration or error handling), consult the official documentation of the library you've generated and the language you are using.
***
## Support
If you run into issues generating or using your client, feel free to reach out to us:
* Contact our support team directly.
* File an issue or submit a pull request in our [GitHub repository](https://github.com/moderationapi/moderation-api).
We appreciate your feedback and contributions, as your suggestions help us improve the experience for everyone.
# Integrations
Source: https://docs.moderationapi.com/resources/integrations
See our integrations and no-code solutions.
## Can't find the integration you're looking for?
If you can't find the integration you're looking for, please [contact us](https://moderationapi.com/sales) and we'll be happy to help you.
# SDKs
Source: https://docs.moderationapi.com/resources/sdks
Moderation API's server-side helper libraries (also known as server-side SDKs) reduce the amount of work required to use the REST APIs, starting with reducing the boilerplate code you have to write.
# Building content views
Source: https://docs.moderationapi.com/review/content-views
Create a saved, filtered slice of your content for your team
A content view is a filter you saved and gave a name. Build one when a group of
moderators, or a recurring task, deserves its own list.
## Create a view
There are two ways in. Start from the sidebar when you know what the view is for; start
from the filter bar when you've been exploring and want to keep what you found — see
[saving what you filtered](#filters-you-set-vs-the-views-own).
Press **+** next to *Content views* in the review sidebar. Give it a name, an
icon and an optional description.
A new view starts with no filters at all, so it shows *everything* you've submitted
— including the content that was handled automatically, which the inbox leaves out.
Narrow it with **Filter**.
Filters you set are yours alone until you press **Save**. Choose **Save to this
view** to make them the view's definition for everyone.
## Filter properties
Open **Filter** and pick a property. Values inside one property are OR'd together
("German *or* French"); separate properties are AND'd ("German *and* flagged for spam").
| Property | Selects |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Label** | Content a policy label applies to. See [label rules](#label-rules) below. |
| **Channel** | The [channel](/get-started/concepts#channel) the content was submitted to. |
| **Action taken** | Content an [action](/actions/content-actions) has been run on — your own escalation, appeal or user-report actions. |
| **Recommendation** | What the automated decision recommended: review, allow or reject. |
| **Severity** | How severe the item scored, as a percentage. See [severity bands](#severity) below. |
| **Casebook** | What the [casebook](/casebook/overview) said: allowed, rejected, or no match. |
| **Casebook vs severity** | Where the casebook and severity triage disagreed ("overruled") or agreed. |
| **Flag status** | Flagged, not flagged, or [shadow flagged](/content-moderation/thresholds). |
| **Date** | A rolling window ("Last 7 days") or a fixed range. Rolling windows keep rolling — a view set to 7 days always shows the last 7 days. |
| **Content type** | What the content *is*: profile, message, post, comment, product, review, event, voice. Taken from the `contentType` you submit. |
| **Media type** | The format: text, image, video, audio or object. |
| **Trust level** | The author's [trust level](/users/trust-levels). |
| **Language** | The detected language. |
| **Author**, **Content ID**, **Conversation** | One specific author, item or thread. |
**Author**, **Content ID**, **Conversation** and the search box are for looking
something up right now. They're never saved into a view.
### Label rules
A label can be filtered four ways. Pick the rule when you add the label, or click its
pill to change it later.
| Rule | Matches |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Detected** / **Has matches** | The label was produced at all, whether or not it flagged. Named *Has matches* for wordlists, PII and link checks, which report concrete matches. |
| **Flagged** | The label flagged the content. |
| **Not flagged** | The label didn't flag it. |
| **Custom threshold** | The label's confidence falls in a range you set, e.g. 80–100%. |
**Detected** is the one worth remembering: it finds content a policy noticed but
deliberately let through — see [wordlist monitoring](/review/recipes/wordlist-monitoring).
### Severity
Severity is one number for how bad an item is, from 0 to 100%. It's the same figure
the **Severity** column shows, so a view and a badge never disagree.
| Band | Range |
| ---------- | ------------- |
| **High** | 70% and above |
| **Medium** | 40–69% |
| **Low** | up to 39% |
Pick a band, one of the *Above / Below 50%* shortcuts, or type your own bounds. Leaving
a bound empty means no limit on that side, so a chip can read *70% and above* rather
than pretending to be a range.
## Describe a view instead of building it
**Filter → AI filter** turns a sentence into filters. Describe the slice you want —
"German listings flagged for spam in the last 7 days" — and the properties are filled
in for you.
It edits what's already on screen rather than starting over, so you can keep going in
words: *"also exclude spam"* adds to the filter, *"drop the date limit"* removes it.
The result is ordinary pills — check them, adjust anything that's off, then save.
It can only use labels, channels and actions that exist in your project, so it works
best once your policies and actions are set up.
## Filters you set vs. the view's own
The pills under the filter bar show only where you've **diverged** from the saved
view. The view's own filters stay invisible: they're the baseline, not a change you
made. A pill reading *Any* means you cleared something the view restricts.
* **Reset** puts the view back the way it's saved.
* **Save → Save to this view** rewrites the view's definition. It's shared, so
everyone gets it.
* **Save → Create new view…** keeps the current view untouched and splits your
filters off into a new one.
On the inbox, only *Create new view* is offered — the inbox itself can't be narrowed.
Changing a view's filters is safe. Nothing is deleted, the list is simply
recalculated, and anything already resolved stays resolved if it comes back.
## Sharing and ordering
Views belong to the project, not to you: a filter you save to a view, and the order you
drag views into in the sidebar, is what everyone else sees too.
When you invite [moderators](/review/inviting-moderators) you choose which views they
can see, which is how a view becomes one team's queue.
# Integrating with your application
Source: https://docs.moderationapi.com/review/integrations
We offer integrations with many popular platforms. These integrations will automatically integrate your platform with the moderation queue and set up actions for you.
You can see a list of available integrations here: [https://moderationapi.com/integrations](https://moderationapi.com/integrations)
[Please reach out to us if you want access to any of our integrations](https://moderationapi.com/sales).
# Inviting moderators
Source: https://docs.moderationapi.com/review/inviting-moderators
Moderators are the people that will be reviewing your content in the moderation queues. Moderators don't have access to other parts of your dashboard, such as the settings, projects, or analytics.
You can invite as many moderators to your as you want.
Adding team members may incur additional costs. Learn more about
[pricing](https://moderationapi.com/pricing).
## How to invite moderators
To invite moderators, go to the **Team** page in the sidebar and click on the **Invite moderators** button. You'll be asked to enter their email address and to select the queues they'll be moderating.
[Invite moderators](https://dash.moderationapi.com/moderation/team)
Moderators will receive an email with a link to accept your invitation. Once they accept it, they'll be able to log in to your dashboard and start moderating your content.
## How to remove moderators
To remove a moderator, go to the **Team** page in the sidebar and click on the **Remove** button next to their name.
[Remove moderators](https://dash.moderationapi.com/moderation/team)
## On demand moderation
We offer on-demand moderation services for your platform. Our team of moderators is trained to moderate your content and can be available 24/7.
If you're interested in this service, please [contact us](https://moderationapi.com/sales).
# Review
Source: https://docs.moderationapi.com/review/overview
The inbox, content views, and how content gets reviewed
Review is where people look at content. Everything you submit lands there, and you
decide what a moderator sees by building **content views** on top of it.
## Inbox and content views
Moderation API handles most content automatically — allowed or rejected on the spot.
What's left is what needs a human: the borderline calls, plus anything you've
deliberately chosen to review, like self-harm flags.
**Inbox** is where those land, from every channel. It always exists, you can't narrow
it, and its badge counts what's still pending. It's the safety net: if something needs a
person, it's here even when it matches none of your views.
What the system settled itself isn't in the inbox — there's no decision left to make.
It's all still stored, though, and a view can select it, which is how teams audit their
own auto-rejections.
**Content views** are saved slices of the same content — "Escalations", "German",
"Listings". A view is a filter plus a few workflow settings, shared with everyone on
the project. Views don't copy or move content, they're windows onto it, so one item can
appear in several at once.
Everything you send to a [moderation endpoint](/content-moderation/submit-content)
is stored and can be put in a view. What needs a human decision also shows up in
the inbox.
They open an item, read it in context, and run an
[action](/actions/content-actions) — or correct what the policies got wrong.
Each allow or reject also teaches your [casebook](/casebook/overview), so
similar content can be settled without a review next time.
Resolved items leave the pending list. Whether they leave *other* views too is a
per-view setting.
## Where to go next
Create a view, filter it, and save it for the team.
Ordering, resolve behaviour, deduplication and columns.
Working through a view, item by item.
Views worth copying, and when to use them.
# Channel views
Source: https://docs.moderationapi.com/review/recipes/channel-views
Separate surfaces that have different tolerances
## The problem
Support chat and public reviews run through the same project, but they aren't the same
job. A frustrated customer swearing in a support ticket is normal; the same words on a
public review page are not. Reviewed in one undifferentiated list, moderators apply the
stricter standard to both — or the looser one.
## The view
Filter **Channel → Support chat**. Repeat for each surface that deserves its own
attention.
This lines review up with the way content was already handled: a
[channel](/get-started/concepts#channel) has its own policies and its own flagging
thresholds, so a channel view shows you the output of one specific rulebook rather than
a mix of several.
## Pairing it with settings
Channel views are a natural place for per-surface workflow settings:
* Support chat, where recency matters: **Ordering → Newest first**.
* Listings, where a backlog is fine but nothing should rot: leave **Oldest first**.
If your channels map onto teams, this is also the cleanest way to scope access — give
each team their channel's view when you
[invite them](/review/inviting-moderators).
# Content type views
Source: https://docs.moderationapi.com/review/recipes/content-type-views
Split review by what the content is — listings, profiles, reviews
## The problem
On a marketplace, a product listing, a seller profile and a buyer review are three
different jobs. A listing is checked for counterfeits and off-platform payment; a
profile for impersonation and contact details; a review for fake praise and abuse.
A moderator switching between all three every few items is switching rulebooks every
few items, and the wrong standard gets applied to whichever one they were doing least.
## The view
Filter **Content type** to one kind of content:
| View | Content type |
| --------------- | ------------- |
| Listings | Products |
| Seller profiles | User profiles |
| Reviews | Reviews |
Content type comes from the `contentType` you send with each item, so it works even
when everything arrives through one channel.
Content type is *what the content is* — a profile, a review. Media type is *how it's
formatted* — text, image, video. A listing with photos is content type Products and
media type Image.
## Worth setting even if you don't split yet
Submitting `contentType` costs nothing and can't be backfilled onto content you've
already sent. Set it from the start and these views are available the day you need
them, along with the same split in your analytics.
# Escalations
Source: https://docs.moderationapi.com/review/recipes/escalations
Give hard calls a second pair of eyes without losing them
## The problem
Some decisions shouldn't be made alone: a permanent ban, a legal threat, a borderline
call a moderator has flip-flopped on twice. Without somewhere to put them, they get
guessed at, or parked in a spreadsheet and forgotten.
You also can't just hand the item to a lead by resolving it — resolving is how the
first moderator says *I'm done*, and in most views that clears the item everywhere.
## The view
Create an [action](/actions/content-actions) called **Escalate**. Moderators run
it from the item detail, or with Cmd + a number key.
In the new view, filter **Action taken → Escalate**.
In **Settings**, set **Items leave this view when → Resolved here only**.
## Why the resolve setting matters
*Resolved here only* is what makes this a real handover. The first moderator resolves
the item in their own view and moves on; it stays pending for the lead, who resolves
it again here once they've decided. Two people, two decisions, one item.
Left on *Resolved anywhere*, the first moderator's resolve would clear the item out of
the escalation view before anyone looked at it.
The same shape works for user reports and appeals — one action, one view, resolved
independently. Only the name of the action changes.
# Language views
Source: https://docs.moderationapi.com/review/recipes/language-views
Route content to moderators who can read it
## The problem
A moderator who doesn't read Portuguese can't judge Portuguese. They'll skip it, guess
at it, or lean on a translation for a decision that turns on tone. Meanwhile the
Portuguese speaker on your team is scrolling past German to find their own work.
## The view
One view per language your team covers:
* Filter **Language → German**.
* Name it for the language, spelled out — *German* reads better in a sidebar than *de*.
* When you [invite moderators](/review/inviting-moderators), give each person access
only to the languages they read.
Language is detected automatically, so nothing extra is needed at submission time.
## Handling the long tail
You won't have a view for every language. Two options:
* Leave the rest to the inbox, which every item awaiting review reaches, and let a lead
sweep it.
* Or build one view for the languages you *don't* staff by selecting them together, and
work it with [translation](/review/translation) turned on.
Watch for a language view that never empties — that's usually a staffing signal, not
a filtering one.
# Media review
Source: https://docs.moderationapi.com/review/recipes/media-review
A separate shift for images and video, with wellness protections on
## The problem
Reviewing graphic images and video is not the same work as reading text, and it takes a
different toll. Mixed into one list, a moderator gets no warning before the next item
and no way to pace themselves — and the protections that would help are either on for
everything or off for everything.
## The view
Filter **Media type → Image, Video**.
Media type is the format the content arrived in, independent of what it is: a product
listing with photos is content type Products and media type Image, so this view picks it
up alongside every other image on the platform.
## Protecting the people working it
[Reviewer wellness](/review/wellness) settings matter most here:
* **Blur** images and video until a moderator chooses to look.
* **Greyscale** to take the edge off graphic colour.
* **Start muted** so audio never plays unprompted.
Each moderator sets their own preferences, and you can set defaults for the project.
Pair this with a shorter shift and an explicit rotation. Views make the work
separable; the scheduling is still yours to do.
# New account review
Source: https://docs.moderationapi.com/review/recipes/new-accounts
Treat a flag on a day-old account differently from a flag on a regular
## The problem
Most serious abuse comes from accounts created for it: throwaway sellers, spam rings,
ban evaders. But a flag alone doesn't distinguish them from your best customer having a
bad day — and if both land in the same list, the disposable account gets the same
patient benefit of the doubt as the five-year regular.
## The view
Filter:
* **Trust level → New** (add *Untrusted* if you enforce against it)
* **Flag status → Flagged**
[Trust level](/users/trust-levels) is maintained per author across the whole project,
so it already reflects their history everywhere, not just this channel.
## What it changes
The value isn't only ordering — it's that the same evidence justifies a firmer action
here. A new account with two spam flags is a pattern; a regular with two is a
conversation. Teams typically run this view with stricter actions available
(suspend, ban) than the ones they'd reach for in the main inbox.
Worth pairing with [severity triage](/review/recipes/severity-triage): high severity
*and* a new account is the combination worth interrupting someone for.
# Content view recipes
Source: https://docs.moderationapi.com/review/recipes/overview
Views worth copying, and the problem each one solves
Each recipe starts from a problem a moderation team actually runs into, and ends with
the [content view](/review/content-views) that solves it. Build them from **Filter**
and save with **Save → Create new view**.
## Dividing the work
A second pair of eyes on the calls no one moderator should make alone.
Moderators only see content they can actually read.
Surfaces with different tolerances get different lists.
Listings, profiles and reviews are three jobs, not one.
A dedicated shift for images and video, with wellness protections on.
## Getting to the right items first
When the backlog can't be cleared, clear the worst of it.
A flag on a day-old account is not the same as a flag on a regular.
Two hundred copies of one message, decided once.
## Checking your automation
See what a rule would catch before it catches anything.
Watch terms you've deliberately chosen not to flag.
Find what your policies are missing, not what they caught.
# Severity triage
Source: https://docs.moderationapi.com/review/recipes/severity-triage
When the backlog can't be cleared, clear the worst of it
## The problem
Volume spikes — a campaign, a raid, a bad week — and there are more flagged items than
your team can get through today. Working oldest-first, they spend the afternoon on
mild profanity from Tuesday while a credible threat from an hour ago sits unread.
The queue being empty was never the goal. The damaging content being handled is.
## The view
Two views, worked in order:
Filter **Severity → High (70% and above)**, and set **Ordering → Highest severity**.
This is the list that gets staffed first.
A second view on **Severity → Medium (40–69%)**, worked when there's room.
## Reading the bands
Severity is a single 0–100% score for how bad an item is, and it's the same number the
**Severity** column shows — a view and a badge never disagree. High is 70% and above,
Medium 40–69%, Low up to 39%. You can also type your own bounds if your thresholds sit
elsewhere.
If your High view is never empty, that's a
[threshold](/content-moderation/thresholds) conversation, not a staffing one.
# Shadow flag QA
Source: https://docs.moderationapi.com/review/recipes/shadow-flag-qa
See what a rule would catch before it catches anything
## The problem
You've written a rule, but you don't trust it yet. Turn it on and a mistake hits real
users — content wrongly rejected, people wrongly warned. Leave it off and you never
find out whether it works.
## The view
A [shadow-flagged](/content-moderation/thresholds) rule evaluates every item and
records what it would have done, without affecting the response your app acts on.
Filter **Flag status → Shadow flagged**.
## Reading it
This view is a list of decisions your rule wanted to make. Work it like a proofreading
pass rather than a queue:
* Items you'd have actioned yourself — the rule is right.
* Items you'd have left alone — false positives, and the reason to keep it in shadow.
When the view stops surprising you, enforce the rule. If it's mostly noise, adjust the
[thresholds](/content-moderation/thresholds) and watch again.
Corrections you make here still count. They feed
[adaptive optimization](/custom-models/adaptive-model-optimization) exactly as
corrections in any other view do.
# Spam waves
Source: https://docs.moderationapi.com/review/recipes/spam-waves
Decide once on content that arrives two hundred times
## The problem
A campaign hits: the same crypto giveaway posted from fifty accounts, sometimes
reworded just enough to look different. Reviewed one row at a time, it's two hundred
identical decisions, the queue looks catastrophic, and everything else in it is buried.
## The view
* Filter **Label → Spam**.
* In **Settings**, set **Deduplication → Similar content**.
Near-identical items collapse into a single row with a count. You decide once, and the
whole group goes with it.
## Choosing a deduplication mode
| Mode | Groups | Use when |
| -------------------- | ------------------------------ | ---------------------------------------- |
| **Similar content** | Near-identical wording | Reworded spam, template messages |
| **Exact duplicates** | Byte-identical content | Copy-paste floods |
| **By content ID** | Resubmissions of the same item | The default, for edited content |
| **Off** | Nothing | You need every submission on its own row |
If the same wave keeps arriving, the durable fix is upstream — a
[wordlist](/policies/wordlists) or a [rule](/content-moderation/rules) that rejects it
before it reaches review. The decisions you make here also become cases in your
[casebook](/casebook/overview), so the next wave of the same pitch can be settled
without you. This view is for handling the wave you're already in.
# Unflagged sampling
Source: https://docs.moderationapi.com/review/recipes/unflagged-sampling
Find what your policies are missing, not what they caught
## The problem
Everything you review is content your policies already flagged. That tells you about
false positives — things caught that shouldn't have been — and nothing at all about
false negatives, the abuse that sailed through unflagged. Those are invisible by
definition: no flag, no queue, no complaint until a user makes one.
## The view
* Filter **Flag status → Not flagged**.
* Set **Ordering → Random**.
Random ordering is what makes this a sample rather than a reading of your newest
traffic, which skews by time of day and by whatever campaign is running.
## Working it
This view never empties, and it isn't supposed to. Treat it as a standing habit:
someone reviews twenty items a week and corrects anything that should have been caught.
Those corrections are the point. They feed
[adaptive optimization](/custom-models/adaptive-model-optimization), which is how the
gap you just found stops being a gap.
Narrow it to one channel or content type if you have a specific suspicion — sampling
everything at once takes a lot of items before it says anything.
# Wordlist monitoring
Source: https://docs.moderationapi.com/review/recipes/wordlist-monitoring
Watch terms you've deliberately chosen not to flag
## The problem
Some terms you want to know about but not act on. A competitor's name in a review isn't
a violation. Neither is a customer promising a refund, or a mention of a product you've
had trouble with. You want visibility, not enforcement — and flagging them would bury
your moderators in content that needs no decision.
## The view
A [wordlist](/policies/wordlists) in **Pass** mode matches content and records the
hits without ever flagging it.
Filter **Label →** your wordlist **→ Has matches**.
## Why not "Flagged"
**Flagged** asks *did this label flag the content* — and a wordlist in Pass mode never
does, so the view would be permanently empty. **Has matches** asks the broader
question: *did this label find anything at all*, flagged or not. It's the only rule
that finds content a policy noticed and deliberately let through.
For classifiers, which return a confidence rather than concrete matches, the same rule
is named **Detected**.
This view is a monitoring surface, not a work queue. Set **Ordering → Newest first**
and skim it — there's nothing here that has to be resolved.
# Reviewing content
Source: https://docs.moderationapi.com/review/reviewing-content
Working through a view, item by item
Open the inbox or any [content view](/review/content-views) and you get the same
page: a chart of what's come in, tabs, and the list itself.
## Pending, resolved, all
The tabs switch what the list shows. **Pending** is the work; **Resolved** is what's
already been decided, where you can unresolve an item to look at it again; **All** is
both. Everything else on the page — chart, counts, filters — follows the tab.
## The chart
Each bar is what arrived in that period, split into resolved and still-pending. Click
either series in the legend to hide it.
Drag across the chart to narrow the list to that range — useful when you spot a spike
and want to know what caused it. The range becomes a date chip in the filter bar, so you
can adjust or remove it like any other filter.
Underneath, each label shows how much of it is still open. Clicking a label filters the
list to it — a fast way to see what's actually driving the backlog.
## The list
* Click a row to open it.
* Click the checkmark to resolve it without opening it.
* Tick several rows to act on them together.
Selecting items brings up an action bar with your [actions](/actions/content-actions),
**Resolve**, and **Edit labels** for correcting labels across the whole selection. On
the Resolved tab the button flips to **Unresolve**, so a batch resolved too eagerly
comes back in one step.
Which columns appear is [a view setting](/review/view-settings#columns), including
columns built from your own `metadata`.
## An item up close
The detail panel shows the content itself, the labels that applied and why the item
was flagged, the metadata you submitted, the item's history, and — where relevant —
the rule or threshold that produced the outcome. A **Casebook** tab at the panel's
right edge shows how your
[casebook](/casebook/rulings#precedent-while-you-review) would rule on the item,
with the closest past cases.
Content submitted as HTML renders in full, images and video included. Metadata that
looks like a URL becomes a link back into your own system.
### Conversations
If you submit a `conversationId`, the surrounding messages from that thread appear
inline, so you can read a remark in the exchange it belongs to instead of on its own.
### Correcting a decision
If a label is wrong, remove it and add the right one. Clearing every label means the
content warranted no flag at all.
Corrections are kept and used the next time you run
[adaptive optimization](/custom-models/adaptive-model-optimization), so the policies
drift toward your judgement rather than away from it.
### Translating
Click **Translate** for content in a language you don't read. The setting sticks as
you move between items. See [Translation](/review/translation).
### Sensitive media
Images and video can be blurred, greyscaled or started muted. Each moderator sets
their own preference, and you can set a default for the project — see
[Reviewer wellness](/review/wellness).
### Acting and moving on
Run an action or resolve from the detail panel, and it moves to the next pending item.
On an item that's already resolved, the button reads **Undo resolve** instead.
Cmd + Enter resolves, Cmd + 1–9
runs an action, and the arrow keys move between items.
## Resolving
Resolving means the item has been dealt with; it moves to the **Resolved** tab. You can
resolve from the list, from the selection bar, from the detail panel, or automatically
as part of an
[action](/actions/content-actions#resolving-an-item-when-an-action-runs).
Whether resolving here also clears the item elsewhere depends on the view — see
[items leave this view when](/review/view-settings#items-leave-this-view-when).
## Looking something up
The search box matches content text; each term becomes its own chip, and further terms
narrow the results. **Filter** also takes an author, a content ID or a conversation ID
when you need to pull up one specific thing — a content ID gives you every moderation
request for that item, which is what you want when a user appeals a decision. These
lookups are temporary and never saved into the view.
To investigate an author properly — their history, trust level and past actions —
open their [author profile](/users/overview) rather than filtering the list.
# Getting content into review
Source: https://docs.moderationapi.com/review/submit-content
How content reaches the inbox and your content views
Content reaches review automatically: everything analyzed through a
[moderation endpoint](/content-moderation/submit-content) is stored and appears in every
[content view](/review/content-views) whose filter it matches. There's nothing extra to
call.
The [inbox](/review/overview) is narrower. It holds the items recommended for review —
what wasn't handled automatically, plus anything you've chosen to always look at.
Content allowed or rejected outright is still stored and a view can select it; it just
doesn't queue up for a moderator by default.
What a given view shows is decided by its filter, which you can change at any time —
see [building content views](/review/content-views).
Setting `doNotStore` to `true` means the content is never stored, so it can't be
previewed anywhere in the dashboard, review included.
# Translating queue items
Source: https://docs.moderationapi.com/review/translation
Translate text, object fields, and audio transcripts in the review queue with one click
Moderators can translate any queue item into their preferred language with one click. It works on plain text, string fields inside object content, and audio transcripts.
Translation is tuned for moderation. Slang, slurs, coded language, and euphemisms are kept intact on purpose, since those are usually the words that decide whether content breaks a policy. Cleaning them up would hide what you're trying to catch.
## Using translation
Open an item, press **Translate** in the detail view, and the content swaps into your target language. A small banner shows what was translated from and to.
A few things to know:
* The Translate button is always visible, so it still works when the stored language was wrong to begin with.
* Toggling off shows the original again. Toggling back on is instant while you're still on the same item.
* If the content is already in your target language, you'll see it unchanged.
* Mixed object content only translates the long natural-language fields. Short IDs, URLs, and tokens are left alone.
* Audio items show the translated transcript with a `(translated)` tag next to it.
The toggle stays on as you move through the queue, so you don't have to flip it for every item.
## Choosing a target language
Each reviewer picks their own target language in user settings. English is the default. Per-project defaults aren't supported yet.
## Reuse and rate limits
Translations are reused for 30 days across your organization. If another moderator opens the same item within that window, they get the translation instantly. No point doing the work twice.
Each organization can run up to 60 translations per minute. Hit the limit and you'll see a retry-after toast; wait a few seconds and try again. The cap is there to keep the feature responsive when a lot of moderators are working at once.
## What gets translated
| Content type | Translated |
| ------------------------------------------ | ---------- |
| Text items | Yes |
| Object items, long natural-language fields | Yes |
| Object items, short IDs, URLs, tokens | No |
| Audio transcripts | Yes |
| Images and video frames | No |
Inputs longer than about 20,000 characters aren't translated, so the response stays quick.
Translation only changes what you see in the reviewer UI. The original content is what's saved on the item and used for model feedback.
# View settings
Source: https://docs.moderationapi.com/review/view-settings
Ordering, resolve behaviour, deduplication and columns
**Settings**, next to *Filter*, holds everything about a view that isn't a filter.
Changes save as you make them and apply for everyone.
## Ordering
| Option | Use it when |
| -------------------- | ---------------------------------------------------------- |
| **Oldest first** | The default. Nothing waits forever. |
| **Newest first** | Recency matters more than backlog — live chat, launches. |
| **Highest severity** | Worst first, when you can't get through everything. |
| **Random** | Sampling rather than clearing, e.g. spot-checking quality. |
## Items leave this view when
This is the difference between a shared window and a separate workload.
* **Resolved anywhere** — resolving the item in any view clears it here too. Right for
views that slice the same work, like a [language view](/review/recipes/language-views).
* **Resolved here only** — the view keeps its own list. An item resolved elsewhere
still waits here. Right for a second pair of eyes: escalations, appeals, QA.
The inbox is always *resolved anywhere*.
## Deduplication
Repetitive content collapses into one row with a count, so a spam wave is one decision
instead of two hundred.
| Option | Groups |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **By content ID** | Resubmissions of the same `contentId` — an edited post, a re-checked profile. The newest version's state wins, so content edited after a resolve comes back for review. The default. |
| **Exact duplicates** | Identical content, whoever posted it. Resolving one instance resolves the group. |
| **Similar content** | Near-identical wording — reworded spam, small variations. Resolving one instance resolves the group. |
| **Off** | Nothing. Every submission is its own row. |
## Columns
Each view chooses its own columns. The built-in ones are **Content**, **Channel**,
**Author**, **Submitted**, **Flagged labels**, **Severity** and **Status**, all on by
default.
Under **From your data**, metadata keys found on your recent items are offered too, so
you can put your own order number, room name or plan tier right in the list. Values that
look like a URL render as links back into your system.
Because columns belong to the view, a fraud team's view can lead with author and
metadata while a content team's view stays focused on the content itself.
## Details and deletion
**Edit details** changes the view's name, icon and description. **Delete view** removes
the saved definition only — no content is ever deleted, and everything it showed is
still in the inbox.
The inbox can't be renamed, refiltered or deleted, and only its ordering, deduplication
and columns can be changed.
# Reviewer wellness
Source: https://docs.moderationapi.com/review/wellness
Control how sensitive images and videos appear in the review queue with blur, grayscale, and video muting
Moderation work means looking at things you'd rather not look at. Wellness settings let you take some of the edge off the queue (blurring images, dropping color, muting video by default) without hiding so much that you can't make a decision.
There are two places these get configured:
* Workspace defaults, set by an admin and applied to every reviewer in the project.
* Personal overrides, where each reviewer adjusts the defaults for their own account.
Personal preferences always take precedence. If a reviewer hasn't set anything personally, the project defaults apply. If neither is set, the system falls back to no protection.
Wellness settings are deliberately not available from the review queue itself. Otherwise reviewers would be tempted to turn protections down mid-shift, with the content right in front of them. The settings live on a separate page and use harmless gradient previews, so you never see real queue content while you're adjusting how it should look.
## Personal wellness settings
Each reviewer configures their own preferences from the [wellness settings page](https://dash.moderationapi.com/user-settings/wellness).
### Blur level
You can blur images and video frames at one of seven levels (0 to 6). Zero is no blur. Six is heavy enough that you can tell something is there without seeing the details.
Hovering a blurred or grayscale image briefly reveals the original, for the times you actually need a closer look.
### Grayscale
Show images and videos in grayscale. Color is often what makes graphic content hit hardest, and pulling it out keeps the shape and composition visible while taking the punch out of the rest.
### Mute videos by default
Videos in the detail view start muted. Press play and they unmute on their own. The setting decides what happens *before* you press play, not what happens once you do. That way you don't get blasted with audio you weren't expecting, but you're not fighting the player either when you actually want to hear something.
## Workspace defaults
Admins can set defaults for the whole project on the reviewer wellness page under [project configuration](https://dash.moderationapi.com/project).
Defaults kick in for any reviewer who hasn't set their own preferences. They're useful for queues with rough content, like a CSAM or graphic-violence queue, where you want every reviewer protected on day one and not left to figure it out themselves.
Reviewers can still override the defaults from their personal settings if they want stronger or weaker protection.
For high-risk queues, set a meaningful blur level and turn grayscale on at the project level. Reviewers who want a clearer view can opt out individually, but nobody starts unprotected.
## How settings get resolved
For each item, the queue picks settings in this order:
1. Whatever the reviewer set personally.
2. The project's defaults.
3. The system fallback (no blur, no grayscale, videos unmuted).
Each setting is resolved on its own, so a reviewer can override just the blur level and still inherit the project's grayscale default.
# Todo
Source: https://docs.moderationapi.com/todo
* Update "understanding API response"
* Add info about channels
# Custom data
Source: https://docs.moderationapi.com/users/custom-data
Add custom items and actions to the author detail sidebar from your own backend
Custom data lets you enrich the author detail view with information and actions from your own systems. When a moderator opens an author, we call an endpoint you host and render the items it returns directly in the author's sidebar.
Common use cases:
* A button to impersonate the user in your own admin
* A link to the user's billing or account page
* A badge showing the user's account status
* Internal IDs, plan names, or links to your support tools
## How it works
Enable the integration and set your endpoint URL in the dashboard.
When a moderator opens an author, we send a `POST` request to your endpoint with the author's external id and email.
Your endpoint returns a list of items describing what to display.
The items appear in the author's detail sidebar.
Responses are cached for a short time per author. If your endpoint is slow, errors, or returns an invalid response, the sidebar simply shows nothing — it never blocks the author page.
## Prerequisites
* **Role**: You need the admin or developer role for your project
* **Endpoint**: An `https://` endpoint that accepts `POST` requests and responds within a few seconds
## Configuration
Navigate to **Configure → Custom data** in your project dashboard:
1. Toggle **Enable integration** on
2. Enter your **Endpoint URL** (must be `https://`)
3. Optionally add **request headers** that should be sent on every request (for example, your own API key)
4. Save
## Authenticating requests
So you can confirm that requests genuinely come from us, every request includes your project's webhook signing secret as an `X-Webhook-Secret` header. Compare this value against your project's webhook secret and reject the request if it doesn't match. You can find or rotate this secret on the [Webhooks page](/actions/webhooks).
Any custom headers you configure are sent alongside the `X-Webhook-Secret` header.
## Request
We send a `POST` request with a JSON body:
```json theme={"theme":"nord"}
{
"version": 1,
"project_id": "proj_a1b2c3",
"author_id": "user_12345",
"email": "user@example.com"
}
```
The contract version. Currently always `1`.
The id of the project the author belongs to.
The author's external id — the same `author_id` you send when [submitting content](/api-reference/moderate/analyze-text).
The author's email, if known.
## Response
Respond with status `200` and a JSON body containing a `version` and an `items` array:
```json theme={"theme":"nord"}
{
"version": 1,
"items": [
{ "type": "section", "label": "Billing" },
{
"type": "badge",
"label": "Status",
"value": "Active",
"variant": "success",
"tooltip": "Subscription is in good standing"
},
{ "type": "stat", "label": "Lifetime value", "value": "$1,240" },
{
"type": "button",
"label": "View billing",
"url": "https://dashboard.stripe.com/customers/cus_123",
"variant": "ghost"
},
{ "type": "divider" },
{
"type": "button",
"label": "Impersonate user",
"url": "https://admin.example.com/impersonate/user_12345",
"variant": "outline"
}
]
}
```
A maximum of 30 items are rendered. Any item that doesn't match one of the supported types below is ignored; if the overall response is invalid, nothing is rendered.
### Item types
A label and a value.
* `label` (string, optional)
* `value` (string, required)
* `tooltip` (string, optional)
A colored status pill.
* `label` (string, optional)
* `value` (string, required)
* `variant` (string, optional): one of `default`, `success`, `warning`, `danger`, `info`
* `tooltip` (string, optional)
A button that opens a link in a new tab. Use it for actions and references alike — choose the `variant` to set its visual weight.
* `label` (string, required)
* `url` (string, required): must be an `https://` URL
* `variant` (string, optional): one of `default`, `outline`, `ghost`. Use `ghost` for a lightweight, link-style appearance and `default` or `outline` for a prominent action.
* `tooltip` (string, optional)
A label with an emphasized value, for numbers and key figures.
* `label` (string, required)
* `value` (string, required)
* `tooltip` (string, optional)
A heading that groups the items beneath it.
* `label` (string, required)
A horizontal separator. Takes no fields.
## Example endpoint
```ts theme={"theme":"nord"}
app.post("/moderation/custom-data", async (req, res) => {
// Verify the request came from us
if (req.headers["x-webhook-secret"] !== process.env.MODAPI_SECRET_KEY) {
return res.status(401).end();
}
const { author_id, email } = req.body;
const user = await getUser(author_id);
res.json({
version: 1,
items: [
{
type: "badge",
label: "Status",
value: user.isBanned ? "Banned" : "Active",
variant: user.isBanned ? "danger" : "success",
},
{
type: "button",
label: "Open in admin",
url: `https://admin.example.com/users/${author_id}`,
},
],
});
});
```
# Fraud Detection
Source: https://docs.moderationapi.com/users/fraud-detection
Advanced behavioral analysis and fraud detection signals to identify suspicious users and prevent sophisticated attacks
Fraud detection is only available for enterprise accounts. Contact sales to
learn more about enabling advanced fraud detection capabilities.
Fraud detection goes beyond content analysis to identify sophisticated bad actors who may bypass traditional moderation through behavioral patterns, technical indicators, and network analysis. This system helps you catch coordinated attacks, bot networks, and persistent abuse before they impact your community.
## What is fraud detection and why is it useful
While content moderation focuses on what users post, fraud detection analyzes how they behave. It identifies patterns that indicate:
* **Bot networks and automation** - Detecting non-human behavior patterns
* **Coordinated inauthentic behavior** - Groups working together to manipulate your platform
* **Account takeovers** - Compromised accounts being used maliciously
* **Sock puppet accounts** - Users creating multiple accounts to evade bans
* **Persistent abuse** - Bad actors who adapt their content but maintain suspicious behavior patterns
This enables you to:
* **Catch sophisticated fraud** that evades content filters
* **Prevent coordinated attacks** before they scale
* **Improve moderation accuracy** by combining behavioral and content signals
## How fraud detection works
The system continuously analyzes user behavior and builds risk profiles using data from your existing moderation workflow:
Fraud detection automatically analyzes data from:
* **Content submissions**: All content sent to [moderation endpoints](/api-reference/moderate/analyze-text) is analyzed for behavioral patterns
* **User metadata**: Update users with additional context like IP addresses, device fingerprints, and location data using the [Author API](/api-reference/author/update-author-details)
* **Custom signals**: Enterprise customers can include their own fraud indicators in the analysis (contact support for integration details)
Every submission generates data points including content posting patterns, login locations, network characteristics, and account metadata.
Machine learning models identify suspicious patterns by analyzing:
* Deviations from normal human behavior
* Similarities to known fraud patterns
* Coordinated behavior across multiple accounts
* Technical indicators of automation or deception
The system learns from millions of user interactions to detect even sophisticated fraud attempts.
All fraud signals are weighted and combined into a single, easy to understand, fraud risk score from 0-100:
* **Low risk (0-30)**: Normal user behavior
* **Medium risk (31-70)**: Some suspicious indicators, worth monitoring
* **High risk (71-100)**: Multiple fraud signals, likely malicious
This single score simplifies decision-making and can be used for sorting, filtering, and automated responses.
## Signal categories
Fraud detection analyzes three main categories of signals:
Unusual user behavior that indicates automation or coordination:
**Content Similarity**
* Detection of copy-paste or template-based content
* Users posting nearly identical messages repeatedly
* Coordinated campaigns using similar language patterns
**Bot-like Behavior**
* Posting on exact schedules (every hour, same time daily)
* Inhuman response times or interaction patterns
* Repetitive actions without natural variation
**Activity Anomalies**
* Sudden changes in posting frequency or style
* Activity patterns that don't match typical human behavior
* Coordinated timing across multiple accounts
Technical signals that reveal attempts to hide identity or location:
**VPN and Proxy Usage**
* Access through known VPN services or proxy networks
* Frequent changes in apparent location
* Use of hosting provider IP addresses instead of residential
**Tor Network Detection**
* Access through the Tor anonymity network
* Often indicates attempts to hide identity for malicious purposes
**IP Reputation Analysis**
* IP addresses associated with previous spam or abuse
* Addresses from known botnet or compromised machine ranges
* Residential proxies being used for automation
**Device and Location Anomalies**
* Multiple logins from geographically impossible locations
* Shared device fingerprints across many accounts
* Inconsistent timezone and language settings
Indicators related to account authenticity and verification:
**Email Analysis**
* Use of disposable or temporary email services
* Email domains associated with spam or abuse
* Suspicious patterns in email address generation
**Account Age and Verification**
* Very new accounts engaging in high-risk behavior
* Accounts that avoid identity verification requirements
* Suspicious registration patterns
**Identity Consistency**
* Mismatches between claimed location and technical indicators
* Inconsistent personal information across platforms
* Generated or stolen profile information
## Implementation and usage
Fraud detection results are available through multiple channels:
* **Review queues**: Sort and filter content by fraud risk score to prioritize high-risk users
* **User dashboard**: View detailed fraud signals and risk assessments for individual users
* **API access**: Consume fraud scores and signals programmatically via the [Author API](/api-reference/author/get-author-details) for custom integrations
The unified risk score makes it easy to integrate fraud detection into existing workflows without complex signal interpretation.
***
## Privacy and compliance considerations
Fraud detection involves analyzing user data, so consider:
### Data collection transparency
* Clearly disclose fraud detection in privacy policies
* Explain what data is collected and how it's used
* Provide opt-out mechanisms where legally required
### Data retention limits
* Only retain fraud signal data as long as necessary
* Implement automatic deletion of old analysis data
* Allow users to request deletion of their data
### Geographic restrictions
* Some jurisdictions have strict rules on behavioral analysis
* Ensure compliance with GDPR, CCPA, and local privacy laws
* Consider different detection levels for different regions
***
# User moderation
Source: https://docs.moderationapi.com/users/overview
Learn how to manage users to improve moderation
## Introduction
Enabling user management in Moderation API solves some of the most common moderation challenges for community platforms.
This is done by introducing the concept of authors (users) in the moderation process. Each author has a [trust level](/users/trust-levels) that is automatically adjusted based on their behavior and moderation history.
User management lets you:
* Tailor and improve moderation accuracy based on trust levels
* Auto-approve content from trusted users with a [content rule](/content-moderation/rules)
* Block or suspend users
* Fraud detection using behavioral analysis
Access the user dashboard to view and manage your users.
Access endpoints to read and update author details.
## Get started
Just add an `authorId` when submitting content to a [moderation
endpoint](/api-reference/moderate/analyze-text).
We recommend using the user ID from your system
but any unique identifier like email or username will work.
You should now see your users appear in the [user dashboard](https://dash.moderationapi.com/project/latest/authors).
There might be a delay in user creation and trust level calculation. This is
because we process users in batches to optimize performance.
## Typical workflow
Here's a common workflow for implementing user management in your moderation system:
Start by including an `authorId` when submitting content for moderation:
```javascript theme={"theme":"nord"}
const response = await fetch("https://api.moderationapi.com/v1/moderate", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
content: {
type: "text",
text: "User's message content",
},
authorId: "user_123",
}),
});
```
Authors are automatically created on first submission.
[Add additional author information](/users/updating-authors) to improve trust scoring and enable fraud detection:
```javascript theme={"theme":"nord"}
await fetch("https://api.moderationapi.com/v1/authors/user_123", {
method: "PUT",
headers: { Authorization: "Bearer YOUR_API_KEY" },
body: JSON.stringify({
name: "John Doe",
email: "john@example.com",
metadata: {
email_verified: true,
is_paying_customer: true,
},
}),
});
```
Check the user dashboard or use the API to monitor [trust levels](/users/trust-levels):
* **New users (Level 0)**: Extra scrutiny for first-time posters
* **Established users (Level 2+)**: Reduced moderation overhead
* **Problematic users (Level -1)**: Automatic flagging for review
When you identify problematic users, take action from the [user dashboard](https://dash.moderationapi.com/project/latest/authors):
The user's status will be updated and your application can respond accordingly.
Before allowing users to post, check their status and respond appropriately:
```javascript theme={"theme":"nord"}
const author = await fetch(`/api/v1/authors/${userId}`);
const userData = await author.json();
if (userData.status !== "enabled") {
if (userData.status === "suspended") {
const suspendedUntil = new Date(userData.block.until);
throw new Error(
`Account suspended until ${suspendedUntil.toLocaleDateString()}`,
);
} else if (userData.status === "blocked") {
throw new Error("Account has been permanently blocked");
}
}
// User is enabled - allow posting
await publishContent(content);
```
Set up [webhooks](/actions/webhooks) to automatically notify users of moderation actions:
```javascript theme={"theme":"nord"}
// Webhook handler for user actions
app.post("/webhook/user-actions", (req, res) => {
const event = req.body;
if (event.type === "author.suspended") {
const { author } = event.data.object;
const reason = author.block?.reason;
const until = author.block?.until; // Unix ms timestamp
sendEmail(author.email, {
subject: "Account Temporarily Suspended",
body: `Your account has been suspended. Reason: ${reason}`,
unsuspendDate: until ? new Date(until) : undefined,
});
}
res.status(200).send("OK");
});
```
# Trust Levels
Source: https://docs.moderationapi.com/users/trust-levels
Understand how trust levels work and how to use them in your moderation workflow
Trust levels provide a systematic way to assess user trustworthiness based on their account history and content behavior. This system helps you implement graduated moderation policies, from strict oversight for new or problematic users to lighter moderation for established, trusted community members.
## What are trust levels and what are they useful for
Trust levels automatically categorize users into reliability tiers based on objective criteria like account age, content quality, and violation history. This enables you to:
* **Apply targeted moderation policies** - Review content from untrusted users more carefully while fast-tracking trusted users
* **Implement progressive restrictions** - Limit posting frequency or features for new users while giving established users more freedom
* **Automate community management** - Let reliable users self-moderate while focusing human attention on higher-risk accounts
* **Reduce false positives** - Use trust context to make better moderation decisions and avoid over-moderating good users
## Our specific levels -1 to 4
The system uses six progressive trust levels, from -1 (Untrusted) to 4 (Trusted):
### Level -1: Untrusted
**Accounts with high violation rates**
* Applied to users with violation rates above 5%
* Suggested permissions: Increased moderation scrutiny, limited posting frequency
* Use case: Users who consistently violate community guidelines
### Level 0: New
**New accounts with limited privileges while learning community norms**
* Applied to accounts under 7 days old with minimal content
* Suggested permissions: Standard moderation review, limited posting frequency
* Use case: Brand new users who haven't established a track record
### Level 1: Basic
**Proven accounts with demonstrated understanding of community guidelines**
* Requirements: 7+ days old, 5+ pieces of clean content, ≤5% violation rate
* Suggested permissions: Standard moderation policies, normal posting frequency
* Use case: Users who've shown they understand your community rules
### Level 2: Member
**Established community members with consistent positive contributions**
* Requirements: 30+ days old, 25+ pieces of clean content, ≤5% violation rate
* Suggested permissions: Reduced moderation scrutiny, access to member-only features, higher posting limits
* Use case: Regular users with a solid track record
### Level 3: Regular
**Trusted community members with demonstrated long-term positive engagement**
* Requirements: 90+ days old, 50+ pieces of clean content, ≤5% violation rate
* Suggested permissions: Light-touch moderation, can help with community moderation, reports prioritized by system
* Use case: Long-term contributors who help maintain community quality
### Level 4: Trusted
**Exceptional community members manually promoted by staff**
* Requirements: Manual promotion by staff (automatic progression not available)
* Suggested permissions: Minimal moderation oversight, near-moderator privileges
* Use case: Users who have earned special recognition and trust from your team
* See [allowlist a specific author](#allowlist-a-specific-author) for how to promote someone
## How trust levels are calculated
Trust levels are calculated using three main factors:
### Account Age
The number of days since the user first appeared in your system. Older accounts are generally more trustworthy as they represent sustained engagement.
### Clean Content Count
The number of non-flagged content pieces from the user's recent activity. The system analyzes the last 100 pieces of content submitted for moderation.
### Violation Rate
The percentage of flagged content in the recent activity window. Users with violation rates above 5% are automatically assigned the Untrusted level (-1), regardless of other factors.
### Calculation Process
1. The system checks violation rate first - if above 5%, assigns Untrusted level
2. For users with acceptable violation rates, it evaluates the highest level where all requirements are met
3. Users progress through levels automatically as they meet criteria, except for Trusted (level 4) which requires manual promotion
**Important**: Trust level calculations are subject to change as we learn more about effective community management. However, the levels themselves and their meanings will remain stable, ensuring your moderation policies won't break with algorithm updates.
### Customization Options
You can customize several parameters to fit your community's needs:
* **New user threshold**: Adjust what constitutes a "new" user (default: under 7 days)
* **Violation rate threshold**: Modify the 5% violation rate that triggers Untrusted status
* **Content requirements**: Adjust the minimum clean content needed for each level
* **Age requirements**: Modify the minimum account age for each trust level
Contact support to discuss customization options for your specific use case.
## Examples of how to use trust levels
Trust levels work best when integrated into your existing moderation workflow:
### Filters in review
Trust levels help you prioritize [review](/review/overview) by focusing moderator attention on higher-risk users.
The simplest approach is to add a **Trust level** filter in the [filter bar](/review/content-views#filter-properties) to handle the riskiest content first.
More advanced strategies include saving trust-level filters as [content views](/review/content-views):
**Create priority views based on trust levels:**
* **High-priority view**: Filter for content from Untrusted (-1) and New (0) users that need immediate attention
* **Standard view**: Content from Basic (1) and Member (2) users for regular review
* **Low-priority view**: Content from Regular (3) and Trusted (4) users that rarely needs intervention
**Other view ideas:**
* **New users**: Only show content from users with trust level 0
* **Trusted user escalations**: Only show flagged content from trust level 3+ users (likely false positives)
* **Violation patterns**: Combine the trust level filter with specific labels to catch concerning behavior early
### Automated Moderation Rules
Incorporate trust levels into your application code to automate moderation decisions:
```javascript theme={"theme":"nord"}
// Example: Apply different review thresholds based on trust level
if (user.trustLevel <= 0) {
// New/Untrusted users: review all content
awaitReview(content);
} else if (user.trustLevel >= 4) {
// Trusted users: approve unless the content is severe
if (severityScore > 0.9) {
awaitReview(content);
} else {
autoApprove(content);
}
} else if (user.trustLevel >= 3) {
// Regular users: light review
if (severityScore > 0.5) {
awaitReview(content);
} else {
autoApprove(content);
}
} else {
// Basic/Member users: standard review
if (severityScore > 0.3) {
awaitReview(content);
} else {
autoApprove(content);
}
}
```
Most channels don't need this in application code — [content
rules](/content-moderation/rules) express the same logic in the dashboard and
return the result as `recommendation.action`.
### Rate Limiting
Apply different posting limits based on trust level:
* **Untrusted (-1)**: 1 post per hour
* **New (0)**: 5 posts per hour
* **Basic (1)**: 15 posts per hour
* **Member (2)+**: No limits
### Feature Access
Grant access to community features progressively:
* **Basic (1)+**: Can post images
* **Member (2)+**: Can create polls, access member channels
* **Regular (3)+**: Can report content, moderate discussions
* **Trusted (4)**: Can pin messages, temporary mute users
## How to override trust levels
Sometimes you need to manually adjust a user's trust level - for example, promoting a valuable community contributor or restricting a problematic user. Manual overrides are available through both the API and dashboard interface.
A manual override **locks** the level: the user stops progressing automatically and stays where you put them until you clear it.
### Allowlist a specific author
To auto-approve everything one author posts, promote them to `Trusted` (level 4). Every channel ships with an [Allow trusted authors rule](/content-moderation/rules#rules-you-start-with) that returns `allow` for anyone at that level, so the promotion is all you need.
Find the user in the [user dashboard](https://dash.moderationapi.com/project/latest/authors) and open their detail page.
Press the **Moderate** button and choose **Update Trust Level**.
The dialog opens on the author's current, automatically calculated level. Click **Trusted** in the tier bar and confirm, or press **Set trust level manually** to pick from the full list of levels.
Note that `Trusted` is the one level with no automatic path — the dialog lists **Manual Promotion** as its only requirement.
To do the same over the API, set [`manual_trust_level`](/users/updating-authors#trust-level-override) to `4`:
```bash theme={"theme":"nord"}
curl -X PUT https://api.moderationapi.com/v1/authors/user_123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"manual_trust_level": 4}'
```
To undo either one, reopen the dialog and press **Switch to automatic detection**, or set `manual_trust_level` to `null`.
An allowlisted author still can't be blocked while the default rule order is
in place — **Allow trusted authors** is evaluated before **Block banned
authors**, so the allow wins. If you rely on manual promotions, move the block
rule to the top of the channel's
[rules](/content-moderation/rules#order-matters).
Allowlisting changes the recommendation, not the analysis. The author's
content is still scored against your policies, still appears in analytics, and
`flagged` still reflects what was found — so you keep the data even for
authors you never act on.
See [updating users](/users/updating-authors) for the other author fields you can set alongside the override.
***
## Frequently Asked Questions
### Why is my user still at level 0 (New)?
The most common reasons:
* **Insufficient content**: They need at least 5 pieces of clean content to reach Basic level
* **Too young**: Account needs to be at least 7 days old for Basic level
* **High violation rate**: Any violation rate above 5% keeps users at Untrusted (-1) level
### How often are trust levels recalculated?
Trust levels are calculated daily and whenever you query a user's information. The system always uses the most recent data available.
### Can trust levels decrease?
Yes, if a user's violation rate increases above acceptable thresholds, their trust level can drop. For example, a Member who starts posting flagged content may drop to Untrusted level.
Or if a user is inactive for a long period, they may lose their established status and revert to a lower level when they return.
### What happens during system updates to the algorithm?
When we improve the trust level algorithm:
* **Level meanings stay the same**: A "Member" will always represent the same type of user
* **Requirements may adjust**: We might fine-tune thresholds based on data insights
* **Your moderation logic continues working**: Since level meanings are stable, your existing code won't break
### My trusted user got flagged - why are they still Trusted?
Manual promotions (like Trusted level) override automatic calculations. If you've manually set someone to Trusted, they'll stay there regardless of their recent activity. You can remove manual overrides if needed.
The content itself is still analyzed and still reported as flagged — only the recommendation is affected. If you want to see those detections without acting on them, the author's flagged content stays visible in analytics and on their detail page.
### I blocked a trusted user but their content is still allowed
The default channel rules evaluate **Allow trusted authors** before **Block banned authors**, and the first match wins. Move the block rule above the allow rule in the channel's [rules](/content-moderation/rules#order-matters), or clear the author's manual trust level override.
# Updating authors
Source: https://docs.moderationapi.com/users/updating-authors
Update authors with data to improve trust scoring and dashboard details
You can update author information to improve trust scoring and fraud detection in your moderation system. Author updates can be made through either the API or the dashboard interface.
## Author Creation
Authors are automatically created when an `author_id` is included in [submit content requests](/api-reference/moderate/analyze-text). However, you can also manually create authors using the API before submitting content.
We recommend adding detailed author information using the update endpoint after automatic creation, or creating authors ahead of time with complete details to improve initial trust scoring accuracy.
## Prerequisites
* **Dashboard**: You need the moderator role for your project
* **API**: Any valid API key for the project
## Available Fields
### Basic Information
These fields are displayed in the dashboard for easier author management:
The author's display name
Author's email address
URL to the author's profile image
URL to the author's profile on your platform or other external resources. Will
add a clickable link in the dashboard.
### Metadata
A flexible JSON object that can store any additional author information
* **Limits**: Maximum 25 keys with 1 level of nesting
* **Usage**: Included in webhooks and author API responses
* **Purpose**: Store custom data relevant to your use case
```json Example theme={"theme":"nord"}
{
"metadata": {
"account_type": "premium",
"signup_source": "mobile_app",
"custom_tags": ["verified_creator", "high_engagement"],
"preferences": {
"notifications": true,
"public_profile": false
}
}
}
```
#### Built-in Metadata Fields
These special metadata fields are used for trust levels and fraud detection, and are rendered in the dashboard:
Boolean indicating if email is verified
Boolean indicating if phone number is verified
Boolean indicating if identity has been verified
Boolean indicating if the user is a paying customer
```json Built-in metadata example theme={"theme":"nord"}
{
"metadata": {
"email_verified": true,
"phone_verified": false,
"identity_verified": true,
"is_paying_customer": true
}
}
```
### Trust Level Override
Override automatic trust level calculations for this author
* **Range**: -1 to 4, or `null` to reset to automatic calculation
* **Use cases**: Permanently promote or demote specific users
* **Reset**: Set to `null` to return to automatic trust level calculation
* **Impact**: Affects moderation decisions and user permissions
Learn more about trust levels and how they work in our [Trust Levels Article](/users/trust-levels).
### Timestamps
These timestamp fields help with trust scoring and fraud detection:
When the author was first encountered in your system (timestamp in
milliseconds)
The author's most recent activity timestamp (timestamp in milliseconds)
Both fields accept timestamp format and contribute to the automatic trust level calculation.
## Making Updates
Use the [PUT /authors/ endpoint](/api-reference/author/update-author-details) to programmatically update author information. If you need to create a new author, use the [POST /authors/ endpoint](/api-reference/author/create-a-new-author).
Here's a complete example of updating an author with all available fields:
```json theme={"theme":"nord"}
{
"name": "John Doe",
"email": "john.doe@example.com",
"profile_picture": "https://example.com/avatars/johndoe.jpg",
"external_link": "https://myplatform.com/users/johndoe",
"metadata": {
"email_verified": true,
"phone_verified": true,
"identity_verified": false,
"is_paying_customer": true,
"account_type": "premium",
"signup_source": "mobile_app",
"preferences": {
"notifications": true
}
},
"manual_trust_level": 3,
"first_seen": 1577836800000,
"last_seen": 1672531200000
}
```
Navigate to the [Users section](https://dash.moderationapi.com/project/latest/authors) in your project dashboard to update user details through the web interface.