# 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](https://dash.moderationapi.com/upgrade)** > > 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. > **[Enable Pay-as-you-go](https://dash.moderationapi.com/billing)** > > 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 quota usage](https://dash.moderationapi.com/billing/usage)** > > 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: > **Block (Permanent Ban)** > > 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 > **Suspend (Temporary Ban)** > > 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 > **Enable (Remove Block/Suspension)** > > 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 **Dashboard** 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** ![User timeline showing content submissions and moderation actions](https://mintcdn.com/moderationapi/FQo1d7iPfIFMaI8P/actions/timeline.png?fit=max\&auto=format\&n=FQo1d7iPfIFMaI8P\&q=85\&s=0b0d29fc506b7f909e433d63b320291b) 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. **Author API** ```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 > **Can blocked users still view content?** > > 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. > **What happens to existing content when a user is blocked?** > > 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. > **Can suspended users see their suspension status?** > > 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. > **How do I handle ban appeals?** > > 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. > **Can I modify suspension periods after they're set?** > > 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. > **Do moderation actions work with trust levels and fraud detection?** > > 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. ![Review queue for reviewing and improving automated moderation](https://mintcdn.com/moderationapi/rnXd3ZotQdidNR7k/images/review-queue.png?fit=max\&auto=format\&n=rnXd3ZotQdidNR7k\&q=85\&s=622b6987a3782d8d908d40e8f11bb7b1) 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: > **[Content actions](/actions/content-actions)** > > 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. > **[Author actions](/actions/author-actions)** > > 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 > **[Content actions](/actions/content-actions)** > > Reject, allow, and custom item-level actions. > **[Author actions](/actions/author-actions)** > > Block, suspend, enable, and custom user-level actions. > **[Webhooks](/actions/webhooks)** > > 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`. **id** `string` *required* Stable event ID, prefixed with `evt_`. Identical across retries — use it to dedupe. **type** `string` *required* The event type, e.g. `queue_item.rejected` or `author.blocked`. See the [event catalog](#event-catalog). **api\_version** `string` *required* Always `v2`. **created** `string` *required* ISO 8601 timestamp of when the event was emitted. **data** `object` *required* Wraps the event-specific payload. **properties** **object** `object` *required* 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 */ } } } ``` > [!TIP] > > Need typed payloads? The full OpenAPI schema for every event lives under > `components.schemas` in our [OpenAPI > spec](https://api.moderationapi.com/v1/openapi.json) > — 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`. | > [!NOTE] > > **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 metaTypeOf = (event: WebhookEvent) => (event.data.object.item as { meta_type?: string } | undefined)?.meta_type; const route = (event: WebhookEvent) => { switch (event.type) { case "queue_item.rejected": switch (metaTypeOf(event)) { case "event": return rejectEvent(event); case "profile": return rejectProfile(event); case "review": return rejectReview(event); } break; case "queue_item.allowed": switch (metaTypeOf(event)) { case "event": return allowEvent(event); case "profile": return allowProfile(event); case "review": return allowReview(event); } break; } return null; }; ``` > [!TIP] > > 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. The SDKs do this for you. They read the secret from the `MODAPI_WEBHOOK_SECRET` environment variable and return the parsed event, or throw an error if the signature does not match. The webhook secret is not your API key. Verification needs the raw request body, byte for byte. If your framework parses the JSON body and you serialize it again, the bytes can change and the check fails. In Express, use `express.raw({ type: "application/json" })` on the webhook route. ```ts TypeScript (SDK) theme={"theme":"nord"} import { validateWebhook } from "@moderation-api/sdk"; // Next.js App Router, Hono, Bun, Cloudflare Workers, or any Fetch API Request. // Also accepts an Express or Next.js Pages Router request with the raw body. export async function POST(request: Request) { let event; try { event = await validateWebhook({ request }); } catch { return new Response("Invalid signature", { status: 400 }); } switch (event.type) { case "queue_item.rejected": // handle reject break; case "author.blocked": // handle block break; // ... } return Response.json({ received: true }); } ``` ```python Python (SDK) theme={"theme":"nord"} from flask import Flask, request from moderation_api.webhook_signature import WebhookSignatureError, construct_event app = Flask(__name__) @app.post("/webhooks/moderation") def moderation_webhook(): try: event = construct_event( request.get_data(), # the raw body, not parsed JSON request.headers.get("modapi-signature", ""), ) except WebhookSignatureError: return "Invalid signature", 400 if event["type"] == "queue_item.rejected": ... # handle reject elif event["type"] == "author.blocked": ... # handle block return {"received": True} ``` ```php PHP (SDK) theme={"theme":"nord"} [!WARNING] > > 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. # What your agent can do Source: https://docs.moderationapi.com/ai/examples Prompts to give an AI agent that is connected to the Moderation API MCP server: queue triage, author checks, policy tuning, incidents and reports. These prompts work with an agent that is connected to the [MCP server](/ai/mcp). Change the details to fit your platform. ## Triage the review queue > Go through today's unresolved items in the review queue. Resolve the obvious spam, and show me the five you are not sure about. The agent lists your queues and their items, decides the clear cases, and resolves them in bulk. You only look at the hard ones. You show as the reviewer of each item it resolves. ## Check an author > Is user 8812 a bad actor? Check their history, and block them if it is a pattern. The agent reads the author's trust level, flagged content and past actions. If you agree, it runs your block action, with a reason. ## Tune a policy from examples > These 10 comments were flagged, but they are fine. These 3 got through, but they should not. Fix it. The agent submits the examples again without storing them, with changed policy settings, until the result is right. Then it shows you the change and updates the channel when you agree. ## Respond to an incident > In the last hour we got a wave of crypto scam posts. Find them, block the authors, and add the domains to a blocklist. The agent finds the posts in the queue, runs your block action on the authors, and adds the domains to a wordlist. ## Get a report > How did moderation go this month? Which policy flags the most, and is anything getting worse? The agent reads the [analytics](/api-reference/analytics/summary): totals with the trend, the most flagged policies and authors, and the numbers per channel. ## Set up a new project > Set up moderation for our new marketplace: a channel for listings and one for messages, a webhook to our endpoint, and a wordlist that blocks payment outside the platform. The agent creates the project, the channels, the webhook and the wordlist, and sends test content to check the setup. ## Work with your other tools When your agent also has other tools connected, it can combine them. For example: * Post a daily summary of the review queue in your team chat. * Open an issue in your tracker when a policy keeps flagging content that moderators allow. * Draft a reply to a user whose post was removed. # MCP server Source: https://docs.moderationapi.com/ai/mcp Connect Claude, Cursor, ChatGPT and other AI agents to your Moderation API account with the hosted MCP server. The Moderation API MCP server lets an AI agent work with your account: go through review queues, look up authors, change policies and wordlists, read analytics and more. It is hosted at: ``` https://mcp.moderationapi.com/mcp ``` The server has one tool for each endpoint of the [REST API](/api-reference/introduction). To add moderation to the code of your app, use the [setup skill](/ai/skill) instead. ## Connect your agent **Claude Code** ```bash theme={"theme":"nord"} claude mcp add --transport http moderation-api https://mcp.moderationapi.com/mcp ``` Or install the [plugin](/ai/overview#start-here), which adds the MCP server and the setup skill together. **Claude and Claude Desktop** Open **Settings → Connectors → Add custom connector**, and enter `https://mcp.moderationapi.com/mcp`. **Cursor** Add the server to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` for all projects: ```json theme={"theme":"nord"} { "mcpServers": { "moderation-api": { "url": "https://mcp.moderationapi.com/mcp" } } } ``` **Other clients** Add a remote MCP server with the URL `https://mcp.moderationapi.com/mcp` and the Streamable HTTP transport. ## Sign in The first time the agent uses the server, your browser opens: 1. Sign in with your Moderation API account. 2. Pick the organization the agent can use. 3. Approve the connection. You need the **Owner**, **Admin** or **Developer** role in the organization. The agent can reach the projects that you can reach. Each connection gets its own API key. It shows on the **API keys** page, named after the app and your email. Delete the key to end the connection. When the agent resolves queue items or runs actions, the review queue shows you as the reviewer. ### Without a browser Agents that cannot open a browser, for example in CI, can send an API key instead: ```bash theme={"theme":"nord"} claude mcp add --transport http moderation-api https://mcp.moderationapi.com/mcp \ --header "Authorization: Bearer $MODAPI_SECRET_KEY" ``` A project key reaches one project. An organization key reaches the projects you allow it to. See [Authentication](/api-reference/authentication). ## Projects Tools that work on content in a project take an optional `projectId`. You can give the project ID or its name. When the agent leaves it out and the key reaches more than one project, the tool returns the projects to choose from. ## What the tools can do | Area | Tools | | - | - | | Content | Submit content and read the result, with or without storing it | | Review queues | List queues, list items, resolve and unresolve items, one by one or up to 100 at a time, queue statistics | | Authors | List, look up, create and update authors | | Actions | List and run moderation actions on content and authors, create and change actions | | Policies | Read and change channels and their policies, create and change wordlists | | Analytics | Summary, most flagged policies and authors, numbers per channel, volumes over time, automation | | Projects | List, create, change and duplicate projects | | Webhooks | List, create and change webhooks | | Feedback | Send a feature request to the Moderation API team | Tools that only read data are marked as read-only, so your agent can run them without asking you. Tools that change or delete data are marked as such, and most agents ask you before they run them. See [What your agent can do](/ai/examples) for prompts to start with. ## Limits The MCP server calls the same API as your code. [Rate limits](/api-reference/rate-limits), quotas and request logs work the same way. # Build with AI agents Source: https://docs.moderationapi.com/ai/overview Use Moderation API from AI coding agents and assistants: a setup skill, an MCP server, a CLI and docs that agents can read. Moderation API has three tools for AI agents. Each one does a different job. | Tool | Use it to | Install | | - | - | - | | [Setup skill](/ai/skill) | Add moderation to the code of your app | `npx skills add moderation-api/skills` | | [MCP server](/ai/mcp) | Run your account from an agent: review queues, authors, policies, analytics | `https://mcp.moderationapi.com/mcp` | | [CLI](/quickstart/cli) | Sign in, create a project, write the keys, check your setup | `brew install moderation-api/tap/modapi` | ## Start here **Any agent** Copy this prompt into your coding agent. Nothing to install first: ```text theme={"theme":"nord"} Read https://moderationapi.com/SKILL.md and set up Moderation API in this app. ``` The agent reads the setup skill from our site and follows it. To keep the skill in your agent for later, install it instead: `npx skills add https://moderationapi.com`. **Claude Code** The plugin installs the setup skill and connects the MCP server in one step: ```bash theme={"theme":"nord"} /plugin marketplace add moderation-api/skills /plugin install moderation-api@moderation-api ``` Then ask: "Set up Moderation API in this app." **Other coding agents** Install the setup skill. It works with Cursor, Codex, Windsurf and other agents that read skills: ```bash theme={"theme":"nord"} npx skills add moderation-api/skills ``` Then ask your agent: "Set up Moderation API in this app." To run your account from the agent too, [connect the MCP server](/ai/mcp). **Chat assistants** In Claude, ChatGPT and other assistants that support MCP, add the MCP server as a connector with the URL `https://mcp.moderationapi.com/mcp`. See [MCP server](/ai/mcp). ## What the agent does When you ask an agent with the setup skill to add moderation, it: 1. Checks your setup with `modapi doctor`. 2. Finds every place where users create content, and writes a plan for you to approve. 3. Creates or links your project with `modapi init`. The CLI writes your keys to your env file. The agent never sees them. 4. Adds the moderation call on your server, and handles `allow`, `review` and `reject`. 5. Sets up [webhooks](/actions/webhooks) for decisions that moderators make later. 6. Checks the result against a list of common mistakes, and sends a test. You approve each change. You can start in [shadow mode](/ai/skill#shadow-mode), to see the results before they affect your users. ## Docs for agents These docs are written for people and for agents: * [`llms.txt`](https://docs.moderationapi.com/llms.txt) lists every page. [`llms-full.txt`](https://docs.moderationapi.com/llms-full.txt) has all pages in one file. * Add `.md` to the URL of any page to get it as Markdown, for example `https://docs.moderationapi.com/ai/mcp.md`. * `modapi --schema` prints the exact request body of an API call. ## When something is missing If your agent finds that Moderation API cannot do what you ask, it can draft a feature request for you. It shows you the draft first, and sends it only when you agree. You can also send one with [`POST /feedback`](/api-reference/feedback/send-feedback). We read every request. # Setup skill Source: https://docs.moderationapi.com/ai/skill Let your AI coding agent add Moderation API to your app with the moderation-api-setup skill. The `moderation-api-setup` skill teaches your coding agent how to add Moderation API to an existing app. It follows the [Agent Skills](https://agentskills.io) format, so it works with Claude Code, Cursor, Codex and other agents that read skills. ## Install ```bash Any agent theme={"theme":"nord"} npx skills add moderation-api/skills ``` ```bash Claude Code plugin theme={"theme":"nord"} /plugin marketplace add moderation-api/skills /plugin install moderation-api@moderation-api ``` The Claude Code plugin also connects the [MCP server](/ai/mcp). The source is at [github.com/moderation-api/skills](https://github.com/moderation-api/skills). Then ask your agent: > Set up Moderation API in this app. ## What it does The skill works in this order, and asks you before each change: 1. **Check.** Runs `modapi doctor`, asks you to sign in if needed, and looks for an existing integration. 2. **Plan.** Finds every place where users create content: messages, posts, comments, profiles, uploads. It proposes a [channel](/get-started/concepts) for each kind of content and what to do on `review` and `reject`, and waits for your approval. 3. **Project.** Links your project or creates a new one with `modapi init`, and creates the channels. Your keys go to your env file, never into the chat. 4. **Code.** Adds one moderation helper on your server and calls it from each place in the plan. It sends `authorId`, `conversationId` and `contentId`, so moderators get context and webhooks find the record. 5. **Result.** Acts on `recommendation.action`, saves masked text when the API masks contact details, and fails open when the API is not reachable. 6. **Webhooks.** Adds a route that verifies the signature and acts on decisions that moderators make later. 7. **Verify.** Checks the code against a list of common mistakes, runs `modapi doctor`, and sends a test. ## Shadow mode The skill asks if you want to start in shadow mode. In shadow mode, Moderation API analyzes all content, and the dashboard and review queue show the real results, but every API response says `allow`. Your users see no change while you check the results. When you trust the results, switch shadow mode off. No code change is needed: ```bash theme={"theme":"nord"} modapi projects update --global-flagging-mode ACTIVE ``` ## Next After setup, the agent tells you what you can do with the [MCP server](/ai/mcp), for example triage the review queue or tune policies. See [What your agent can do](/ai/examples). # 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 the moderation actions of the project. The list is paginated. # Update an action Source: https://docs.moderationapi.com/api-reference/actions/update-an-action put /actions/{id} Update an action. # Get the most flagged authors Source: https://docs.moderationapi.com/api-reference/analytics/authors get /analytics/authors Get the authors with the most flagged content in a time window, largest first. # Get the automation breakdown Source: https://docs.moderationapi.com/api-reference/analytics/automation get /analytics/automation Get how flagged content was handled in a time window: settled by rules or the casebook, or sent to human review, and what the reviewers decided. # Get analytics per channel Source: https://docs.moderationapi.com/api-reference/analytics/channels get /analytics/channels Get requests, flags and flag rate for each channel of the project in a time window. # Get the most flagged policies Source: https://docs.moderationapi.com/api-reference/analytics/policies get /analytics/policies Get the policy labels that flagged the most content in a time window, largest first, with the count in the window before. # Get the analytics summary Source: https://docs.moderationapi.com/api-reference/analytics/summary get /analytics/summary Get the totals of the project for a time window: requests, flags, flag rate, allowed, rejected and sent to review, with the window before for the trend. Also the review queue backlog and a flag-rate spike, if there is one. # Get volumes over time Source: https://docs.moderationapi.com/api-reference/analytics/volumes get /analytics/volumes Get requests, flags, rejections and reviews per hour (24h) or per day (7d, 30d, 90d), oldest first. # Authentication Source: https://docs.moderationapi.com/api-reference/authentication How to authenticate with the API The Moderation API uses API keys to authenticate requests. There are two kinds of keys: | Key | Prefix | Use it to | Where to manage it | | - | - | - | - | | Project key | `sk_` | Moderate content and work with a project's authors, actions, review queues, and webhooks. | **Project → Configure → API keys** | | Organization key | `sk_org_` | Create and manage projects and channels with the [management API](/api-reference/management-api), and call any project's endpoints with the `Project-Id` header. | **Settings → API keys** | Each project has its own project keys. A project key can't call the management API. An organization key can call a project's endpoints when it names the project in the `Project-Id` header (`projectId` in the SDKs). See [Call project endpoints](/api-reference/management-api#call-project-endpoints). Both kinds can call [Get account details](/api-reference/account/get-account-details). You manage keys in the [dashboard](https://dash.moderationapi.com). 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. ```bash theme={"theme":"nord"} curl https://api.moderationapi.com/v1/account \ -H "Authorization: Bearer $MODERATION_API_KEY" ``` You must make all API calls over HTTPS. Calls that you make over plain HTTP will fail. API requests without authentication will also fail. > [!NOTE] > > 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. This resets the author: status, blocks, trust level, metrics and action history are removed. The author is created again with a clean record the next time content is moderated for the same ID. # 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 Source: https://docs.moderationapi.com/api-reference/author/list-authors get /authors List the authors of the project, with their status, trust level and content metrics. The list is paginated. By default, the authors that were active most recently come first. # 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 a channel Source: https://docs.moderationapi.com/api-reference/channels/create-a-channel post /projects/{projectId}/channels Create a channel in a project, with the default rules. The project's first channel becomes its default. Needs the `channels:write` permission. # Delete a channel Source: https://docs.moderationapi.com/api-reference/channels/delete-a-channel delete /channels/{id} Delete a channel. Its key is freed for a new channel. When it was the default, the oldest remaining channel becomes the default. A channel that still moderates a Slack workspace must be disconnected first. Needs the `channels:write` permission. # Duplicate a channel Source: https://docs.moderationapi.com/api-reference/channels/duplicate-a-channel post /channels/{id}/duplicate Copy a channel in its project, with every setting, policy, rule and wordlist. Integrations (Discord, Slack, WordPress) are not copied. Needs the `channels:write` permission. # Get a channel Source: https://docs.moderationapi.com/api-reference/channels/get-a-channel get /channels/{id} Get a channel by ID. Needs the `channels:read` permission. # List channels Source: https://docs.moderationapi.com/api-reference/channels/list-channels get /projects/{projectId}/channels List the channels in a project. The list is paginated. Needs the `channels:read` permission. # Update a channel Source: https://docs.moderationapi.com/api-reference/channels/update-a-channel put /channels/{id} Update a channel. Fields you leave out keep their value. Needs the `channels:write` permission. # 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 The HTTP statuses, error codes and response format that Moderation API returns when a request fails. Moderation API uses conventional HTTP statuses. Codes in the `2xx` range mean success. Codes in the `4xx` range mean the request failed because of the information you sent, for example a missing parameter or an invalid API key. Codes in the `5xx` range mean a problem on our side. ## Response format Every error response has the same JSON body: ```json theme={"theme":"nord"} { "type": "not_found_error", "code": "author_not_found", "message": "Author not found", "request_id": "h8fkz6kkzoa446ffve37atys", "doc_url": "https://docs.moderationapi.com/api-reference/errors#author-not-found" } ``` **type** `string` *required* The broad category of the error. One value per HTTP status. See [Statuses](#statuses). **code** `string` *required* Exactly what went wrong. Use this field when your code must react to a specific error. See [Error codes](#error-codes). We can add new codes, so handle codes you don't know by their `type`. **message** `string` *required* A human-readable explanation. Don't parse it; the text can change. **issues** `array` The problems with the request body, when it fails validation. Each issue has a `message` and the `path` of the field. **request\_id** `string` *required* Identifies the request. Every response, also a successful one, has the same value in the `X-Request-Id` header. Include it when you contact support. **doc\_url** `string` *required* A link to the explanation of the `code` on this page. A request body that fails validation returns the problems in `issues`: ```json theme={"theme":"nord"} { "type": "invalid_request_error", "code": "validation_failed", "message": "Invalid input: expected string, received undefined\n → at content.text", "issues": [ { "expected": "string", "code": "invalid_type", "message": "Invalid input: expected string, received undefined", "path": ["content", "text"] } ], "request_id": "ec9srzy93fuf2brgdl0upgas", "doc_url": "https://docs.moderationapi.com/api-reference/errors#validation-failed" } ``` ## Statuses | Status | `type` | Meaning | | - | - | - | | `400` | `invalid_request_error` | The request is malformed or a parameter is invalid. | | `401` | `authentication_error` | The API key is missing, invalid or revoked. | | `403` | `permission_error` | The API key can't do this, or your plan doesn't allow it. | | `404` | `not_found_error` | The resource doesn't exist, or the API key can't see it. | | `409` | `conflict_error` | The request conflicts with the current state of a resource. | | `422` | `invalid_request_error` | The request is valid JSON, but it can't be processed. | | `429` | `rate_limit_error` | Too many requests. See [Rate limits](/api-reference/rate-limits). | | `500` | `api_error` | Something went wrong on our side. Retry with exponential backoff. | ## Error codes ### 400 Bad request | `code` | Meaning | | - | - | | `invalid_request` | A parameter is invalid, or the request body is not valid JSON. `message` says which. | | `validation_failed` | The request body doesn't match the schema. `issues` lists each problem. | | `project_id_required` | An organization key called a project endpoint without the `Project-Id` header, and the organization has more than one project. | | `project_name_ambiguous` | The `Project-Id` header names a project by name, and more than one project the key reaches has that name. Use the project's ID. | ### 401 Unauthorized | `code` | Meaning | | - | - | | `missing_api_key` | The `Authorization` header has no API key. | | `malformed_api_key` | The `Authorization` header doesn't contain a key in a valid format. | | `invalid_api_key` | The API key doesn't exist or was revoked. | | `unauthorized` | Authentication failed for another reason. | ### 403 Forbidden | `code` | Meaning | | - | - | | `permission_denied` | The API key can't perform this request. | | `account_disabled` | Your organization's account is disabled. | | `quota_exceeded` | Your organization has used all API requests on its plan. Upgrade to continue. | | `plan_feature_unavailable` | Your plan doesn't include this feature. | | `plan_limit_exceeded` | The request goes over a limit of your plan. | | `organization_key_required` | This endpoint needs an organization API key. See [Management API](/api-reference/management-api). | | `missing_key_permission` | The organization key doesn't have the permission this endpoint needs. | | `project_not_reachable` | The organization key is limited to other projects. | ### 404 Not found | `code` | Meaning | | - | - | | `resource_not_found` | The resource or endpoint doesn't exist. | | `action_not_found` | The action doesn't exist. | | `author_not_found` | The author doesn't exist. | | `queue_not_found` | The review queue doesn't exist. | | `wordlist_not_found` | The wordlist doesn't exist. | | `webhook_not_found` | The webhook doesn't exist. | | `project_not_found` | The project doesn't exist. | | `channel_not_found` | The channel doesn't exist. | ### Other statuses | Status | `code` | Meaning | | - | - | - | | `409` | `resource_conflict` | The request conflicts with an existing resource, for example a key that is already taken. | | `422` | `unprocessable_content` | The request is valid, but it can't be processed. | | `429` | `rate_limit_exceeded` | Too many requests. Wait for the seconds in the `Retry-After` header. | | `500` | `internal_error` | Something went wrong on our side. Retry with exponential backoff. | ## Handling errors Write code that handles every error, so that a failed request doesn't make your own service unavailable: * Use `code` for errors you handle in a specific way, for example `quota_exceeded`. Fall back to `type` for codes you don't know. * Retry `429` and `5xx` responses with exponential backoff. For `429`, wait at least the seconds in the `Retry-After` header. * Don't retry other `4xx` responses without a change to the request. * Log `request_id`, so that support can find the request. # 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" ], "actionKey": "suspend-author", "value": "Spam" }' ``` ```javascript Node.js theme={"theme":"nord"} import { ModerationApi } from "@moderation-api/sdk"; // Reads your API key from the MODAPI_SECRET_KEY environment variable const moderationApi = new ModerationApi(); // Or pass the key explicitly // const moderationApi = new ModerationApi({ secretKey: "" }); await moderationApi.actions.execute({ actionKey: "suspend-author", contentIds: ["60c9e1c0e4e7e1001c7a0e1e", "60c9e1c0e4e7e1001c7a0e1f"], value: "Spam", }); ``` ```ruby Ruby theme={"theme":"nord"} require "moderation_api" Operations = ModerationApi::Models::Operations client = ModerationApi::ModerationApi.new(secret_key: ENV.fetch("MODAPI_SECRET_KEY")) client.actions.execute(body: Operations::ExecuteActionRequestBody.new( action_key: "suspend-author", content_ids: [ "60c9e1c0e4e7e1001c7a0e1e", "60c9e1c0e4e7e1001c7a0e1f" ], 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" ], "actionKey": "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" ), "actionKey" => "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" ], "actionKey": "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", }, "actionKey": "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) } ``` # Send feedback Source: https://docs.moderationapi.com/api-reference/feedback/send-feedback post /feedback Sends a feature request, bug report or other feedback to the Moderation API team. The team reads every request and can reply by email. Limited to 5 requests per minute per organization. # 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. > **[Quickstart](/get-started/introduction)** > > 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. > **[Integrations](https://moderationapi.com/integrations)** > > See our integrations and no-code solutions. # List moderation actions Source: https://docs.moderationapi.com/api-reference/list-actions get /actions List the moderation actions of the project. The list is paginated. # Management API Source: https://docs.moderationapi.com/api-reference/management-api Create, change, copy, and delete projects and channels from your own code with an organization API key. The management API lets you set up [projects and channels](/get-started/concepts) from your own code instead of the dashboard. Use it when you run moderation for many products or customers and set up the same configuration again and again. You call it with an **organization key**. An organization key manages projects and channels across your organization, and can call every other endpoint for a project you name. See [Call project endpoints](#call-project-endpoints) and [Authentication](/api-reference/authentication). The management API is available on every plan. ## Prerequisites * The **Admin** or **Owner** role in your organization. Only these roles can create organization keys. ## Project limit Your plan sets how many projects your organization can have: self-serve plans include 2, Enterprise plans 25. Your limit shows in **Settings → Usage**. Creating or duplicating a project beyond it returns `403` with the code `plan_limit_exceeded`. Delete a project you no longer need, or contact [support@moderationapi.com](mailto:support@moderationapi.com) for more. ## Create an organization key **1. Open API keys** In the [dashboard](https://dash.moderationapi.com), go to **Settings → API keys** and click **Create key**. **2. Choose its access** * **Name**: a label so you can recognize the key, for example the tool that uses it. * **Permissions**: pick **None**, **Read**, or **Write** for projects and for channels. Write includes read. * **Projects**: leave empty to reach every project, or pick the projects the key can reach. * **Expires**: **Never**, **30 days**, **90 days**, or **365 days**. **3. Copy the key** Copy the key and store it in a secret manager. You can't see it again after you close the dialog. Organization keys start with `sk_org_`, so you and secret scanners can tell them apart from project keys. ## Permissions Each endpoint needs one permission on the key: | Permission | Endpoints | | - | - | | `projects:read` | [List projects](/api-reference/projects/list-projects), [get a project](/api-reference/projects/get-a-project) | | `projects:write` | [Create](/api-reference/projects/create-a-project), [update](/api-reference/projects/update-a-project), [duplicate](/api-reference/projects/duplicate-a-project), and [delete](/api-reference/projects/delete-a-project) a project, and [create an API key](/api-reference/projects/create-a-project-api-key) for a project | | `channels:read` | [List channels](/api-reference/channels/list-channels), [get a channel](/api-reference/channels/get-a-channel) | | `channels:write` | [Create](/api-reference/channels/create-a-channel), [update](/api-reference/channels/update-a-channel), [duplicate](/api-reference/channels/duplicate-a-channel), and [delete](/api-reference/channels/delete-a-channel) a channel | An organization key can also call [Get account details](/api-reference/account/get-account-details), so you can check that the key works. ### Call project endpoints An organization key can call the endpoints of any project it reaches: submit content, list authors, work a review queue, and so on. Name the project in the `Project-Id` header, by its ID or by its name. In the SDKs and the CLI this is the `projectId` parameter. If your organization has one project, you can leave it out. ```bash theme={"theme":"nord"} curl https://api.moderationapi.com/v1/authors \ -H "Authorization: Bearer $MODAPI_ORGANIZATION_KEY" \ -H "Project-Id: $PROJECT_ID" ``` Reading needs `projects:read` on the key, and changing anything (submitting content included) needs `projects:write`. A project key always acts on its own project: naming another project with it returns `403`. A name matches regardless of case. When more than one project the key reaches has that name, the request returns `400` with `project_name_ambiguous` and lists the matching projects: name the project by its ID instead. ### Keys limited to some projects A key limited to some projects only sees those projects and their channels. Other projects return `404 Not Found`. A limited key can't create or duplicate projects, because the new project would fall outside its limit. Use a key that reaches every project for that. ## Example: set up a new project Create a project. The response includes the new project's `secretKey`, which you use to moderate content in that project. The other project endpoints don't return it. You can also find it in the dashboard under **Project → Configure → API keys**. ```bash theme={"theme":"nord"} curl -X POST https://api.moderationapi.com/v1/projects \ -H "Authorization: Bearer $MODERATION_ORG_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Marketplace", "domain": "acme.example" }' ``` Every new project comes with one channel, a review queue, and a secret key. When you give a `domain` or `context`, the project suggests policies for the platform. Add a channel for another surface of your product: ```bash theme={"theme":"nord"} curl -X POST https://api.moderationapi.com/v1/projects/PROJECT_ID/channels \ -H "Authorization: Bearer $MODERATION_ORG_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Reviews", "key": "reviews", "contentType": "review" }' ``` Then moderate content with the project's secret key, and pass the channel's `key` as `channel`: ```bash theme={"theme":"nord"} curl -X POST https://api.moderationapi.com/v1/moderate \ -H "Authorization: Bearer $PROJECT_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "content": { "type": "text", "text": "Great seller, fast shipping!" }, "channel": "reviews" }' ``` ## Copy a setup To reuse a configuration you already tuned, duplicate it instead of building it again: * **[Duplicate a project](/api-reference/projects/duplicate-a-project)** copies its settings, every channel with its policies, rules and wordlists, its moderation actions, and its review queues. The copy gets its own secret key. Webhooks, integrations, API keys, and content aren't copied. * **[Duplicate a channel](/api-reference/channels/duplicate-a-channel)** copies a channel inside its project with every setting, policy, rule, and wordlist. Use it to try a change next to the original. Integrations aren't copied. The channel endpoints change channel settings such as content type, flagging mode, and media settings. To change a channel's policies and rules, use the dashboard, or duplicate a channel that already has them. ## Roll a key To replace a key without downtime, open the key's menu in **Settings → API keys** and click **Roll key**. The new key has the same access and lifetime as the old one. Choose when the old key stops working: **Now**, **1 hour**, **24 hours**, or **7 days**. The old key never outlives its own expiry date. To revoke a key at once, click **Delete**. ## Activity and audit Each key shows when it was last used. Every change made with an organization key shows in **Settings → Activity**, with the key that made it. ## Rate limits The management API allows 60 requests per minute per organization, separate from your moderation [rate limit](/api-reference/rate-limits). When you create many projects in a script, handle `429` responses and retry after the time in the `Retry-After` header. ## Errors | Status | `code` | Reason | | - | - | - | | `401` | `missing_api_key`, `invalid_api_key` | The key is missing, invalid, or expired. | | `403` | `organization_key_required` | The key is a project key. | | `403` | `missing_key_permission` | The key doesn't have the permission the endpoint needs. | | `403` | `project_not_reachable` | The key is limited to other projects. | | `403` | `plan_limit_exceeded` | Creating the project would go over your plan's [project limit](#project-limit). | | `400` | `project_id_required` | A project endpoint was called without `Project-Id`, and the organization has more than one project. | | `400` | `project_name_ambiguous` | More than one project the key reaches has the name in `Project-Id`. Use the project's ID. | | `404` | `project_not_found`, `channel_not_found` | The project or channel doesn't exist, or it's outside the key's project limit. | | `429` | `rate_limit_exceeded` | Too many requests. See [Rate limits](#rate-limits). | See [Errors](/api-reference/errors) for the response format and all codes. # 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). # Pagination Source: https://docs.moderationapi.com/api-reference/pagination How list endpoints return their results page by page. All endpoints that list resources, for example [List authors](/api-reference/author/list-authors) and [List webhooks](/api-reference/webhooks/list-webhooks), return their results in pages. ## Request Send these query parameters to choose a page: **page** `integer` (default: 1) The page to return, starting at 1. **pageSize** `integer` (default: 20) The number of items per page. Maximum 100. ```bash theme={"theme":"nord"} curl "https://api.moderationapi.com/v1/actions?page=2&pageSize=50" \ -H "Authorization: Bearer $MODERATION_API_KEY" ``` ## Response A list response has the items in `data` and the position in the full list in `pagination`: ```json theme={"theme":"nord"} { "data": [ { "id": "act_1", "name": "Suspend for 24 hours" } ], "pagination": { "page": 2, "pageSize": 50, "total": 51, "totalPages": 2, "hasMore": false } } ``` **data** `array` The items on this page. **pagination** `object` **properties** **page** `integer` The current page, starting at 1. **pageSize** `integer` The number of items per page. **total** `integer` The total number of items in the list. **totalPages** `integer` The total number of pages. **hasMore** `boolean` Whether there is a next page. ## Get all items To get the full list, request the next page while `hasMore` is `true`. ## List filters Some list endpoints take filters as query parameters. Send a list filter as repeated keys, for example `?labels=spam&labels=toxicity`, or as one comma-separated value, for example `?labels=spam,toxicity`. Send boolean filters as `true` or `false`. # Create a project Source: https://docs.moderationapi.com/api-reference/projects/create-a-project post /projects Create a project with one channel, a review queue inbox and a secret API key. Give a domain or context and the project suggests policies for the platform. Needs the `projects:write` permission and a key that reaches all projects. # Create a project API key Source: https://docs.moderationapi.com/api-reference/projects/create-a-project-api-key post /projects/{id}/api-keys Create a new secret API key for a project and return it. Give each app or computer its own key, so you can revoke one without breaking the others. The project's existing keys keep working. Needs the `projects:write` permission. # Delete a project Source: https://docs.moderationapi.com/api-reference/projects/delete-a-project delete /projects/{id} Delete a project with everything in it: channels, queues, API keys and stored content. This can't be undone. The project's API keys stop working at once. Needs the `projects:write` permission. # Duplicate a project Source: https://docs.moderationapi.com/api-reference/projects/duplicate-a-project post /projects/{id}/duplicate Copy a project's configuration into a new project: its settings, every channel with its policies, rules and wordlists, its moderation actions and its review queues. The copy gets its own secret API key. Webhooks, integrations, API keys and content are not copied. Needs the `projects:write` permission and a key that reaches all projects. # Get a project Source: https://docs.moderationapi.com/api-reference/projects/get-a-project get /projects/{id} Get a project by ID. Needs the `projects:read` permission. # List projects Source: https://docs.moderationapi.com/api-reference/projects/list-projects get /projects List the projects in the organization. The list is paginated. Needs the `projects:read` permission. # Update a project Source: https://docs.moderationapi.com/api-reference/projects/update-a-project put /projects/{id} Update a project. Fields you leave out keep their value. Needs the `projects:write` permission. # Rate limits Source: https://docs.moderationapi.com/api-reference/rate-limits Learn about API rate limits and how to work with them. Rate limits apply per organization. Each group of endpoints has its own limit, so a busy moderation integration doesn't slow down your other API calls. | Endpoints | Limit | | - | - | | [Submit content](/api-reference/moderate/submit-content) | Your plan's rate limit tier | | [Authors](/api-reference/author/list-authors) | 120 requests per minute | | [Projects](/api-reference/projects/list-projects) and [channels](/api-reference/channels/list-channels) | 60 requests per minute | | All other endpoints: account, actions, wordlists, review queues and webhooks | 300 requests per minute | You can view your organization's 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 `429` response like: ```json theme={"theme":"nord"} { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Rate limit exceeded. Try again in 10 seconds.", "request_id": "h8fkz6kkzoa446ffve37atys", "doc_url": "https://docs.moderationapi.com/api-reference/errors#rate-limit-exceeded" } ``` The response will include the `Retry-After` header indicating how many seconds to wait. See [Errors](/api-reference/errors) for the response format. ## 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 review queue with its item counts. The counts use the filters and the date window of the queue, the same as the dashboard. # List queue items Source: https://docs.moderationapi.com/api-reference/review-queues/get-queue-items get /queue/{id}/items List the items of a review queue, with the filters and the date window of the queue applied. By default only pending items. The list is paginated. Use the item `id` to resolve or unresolve an item. # Get queue statistics Source: https://docs.moderationapi.com/api-reference/review-queues/get-queue-statistics get /queue/{id}/stats Get review statistics of a queue for a period: resolved and pending items, the top reviewers and the resolved items per day. # List queues Source: https://docs.moderationapi.com/api-reference/review-queues/list-queues get /queue List the review queues of the project. The default queue comes first. The list is paginated. To get the item counts of a queue, use `GET /queue/{id}`. # Resolve a queue item Source: https://docs.moderationapi.com/api-reference/review-queues/resolve-a-queue-item post /queue/{id}/items/{itemId}/resolve Mark an item of a queue as resolved. It is the same as a check-off in the dashboard: the item leaves the pending list and a QUEUE_ITEM_COMPLETED webhook is sent. To also take a moderation action, use `POST /actions/execute`. # Resolve queue items Source: https://docs.moderationapi.com/api-reference/review-queues/resolve-queue-items post /queue/{id}/items/resolve Mark up to 100 items of a queue as resolved in one request. Each item is resolved the same as with `POST /queue/{id}/items/{itemId}/resolve`. IDs that do not exist in the project are skipped and returned in `notFoundIds`. # 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 resolved item of a queue as pending again. It is the same as removing a check-off in the dashboard: the item comes back in the pending list. # 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 https://api.moderationapi.com/v1/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 https://api.moderationapi.com/v1/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 https://api.moderationapi.com/v1/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 https://api.moderationapi.com/v1/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 https://api.moderationapi.com/v1/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 https://api.moderationapi.com/v1/openapi.json webhook author.updated Fired when an author's profile or metadata changes. # Create a webhook Source: https://docs.moderationapi.com/api-reference/webhooks/create-a-webhook post /webhooks Create a webhook subscribed to one or more event types. Deliveries use the v2 envelope and are signed with the project signing secret (see the signing secret endpoint). # Delete a webhook Source: https://docs.moderationapi.com/api-reference/webhooks/delete-a-webhook delete /webhooks/{id} Delete a webhook. # Get a webhook Source: https://docs.moderationapi.com/api-reference/webhooks/get-a-webhook get /webhooks/{id} Get a webhook by ID. # Get the webhook signing secret Source: https://docs.moderationapi.com/api-reference/webhooks/get-signing-secret get /webhook-secret Get the signing secret used to sign webhook deliveries for this project, creating one if none exists yet. Verify deliveries by comparing the `modapi-signature` header to HMAC-SHA256(raw request body, secret) hex-encoded. # List webhooks Source: https://docs.moderationapi.com/api-reference/webhooks/list-webhooks get /webhooks List the webhooks of the project. The list is paginated. # Queue item action Source: https://docs.moderationapi.com/api-reference/webhooks/queue-item-action https://api.moderationapi.com/v1/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 https://api.moderationapi.com/v1/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 https://api.moderationapi.com/v1/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 https://api.moderationapi.com/v1/openapi.json webhook queue_item.resolved Fired when a queue item is resolved. # Update a webhook Source: https://docs.moderationapi.com/api-reference/webhooks/update-a-webhook put /webhooks/{id} Update a webhook. Legacy v1 webhooks are read-only: delete them and create a new webhook instead. # 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 # Create wordlist Source: https://docs.moderationapi.com/api-reference/wordlist/create-wordlist post /wordlist Create a wordlist in the organization. You can add words now or later. Each word is embedded in the background for similarity matching. Use the embedding status endpoint to follow the progress. To use the wordlist in moderation, add it to a policy. # 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 the wordlists of the organization. The list is paginated. # 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. Fields that you do not send stay the same. If you send words, they replace all existing words, and the embeddings are updated in the background. # 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 ![The import dialog offering two sources: past review decisions, or a CSV upload](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-import.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=aca4d52c6c742ae09a5d0add99d18258) 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: ![The disagreements stepper: conflicting cases on the left, and the decision panel with Should be allowed, Should be rejected, and Keep both rulings](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-disagreements.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=2e9adb5e4e32d7afbb8bdb49cd400792) * **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 ![The delete dialog showing how many items the case has decided before you confirm](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-delete.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=02d6e299a3830d4d5adba7f41ade656b) 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 ![A queue filter bar combining Casebook allowed with Casebook overruled](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-queue-filters.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=72bf74872af59de1733629741d8e9b7c) 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 [YouTube video player](https://www.youtube.com/embed/lyiPJeuCWlo) 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 **1. Remembered from review decisions** 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. **2. Added by hand** 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. **3. Imported** 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 dialog showing the content, ruling, labels, topic, channel, reviewer, and times applied](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-case-detail.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=0673094d8bb6d943d75d6a14df009d86) 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. > [!NOTE] > > 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." ![A lookup result: the casebook would reject the message, with the closest cases and their similarity](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-lookup.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=7878951440c2b80cc41f5737baa692bd) 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." | ![A lookup that declines to rule: close cases agree but there are not enough of them yet, 58% of the way to ruling on its own](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-lookup-thin.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=1d9af5859780c706b48d2e573838751b) 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 ![The casebook bookmark pulled out beside an item, showing the ruling and the closest cases](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-bookmark.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=36e023fa6b46b88f772c7ee7f0b3de8e) 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. ![The topics browser: a topic tree on the left, and the selected topic's cases, keywords, and ruling tendency on the right](https://mintcdn.com/moderationapi/ig89jGx6tNc_MAYy/images/casebook-topics.png?fit=max\&auto=format\&n=ig89jGx6tNc_MAYy\&q=85\&s=7951fa1f94f9ee73fd9db40438b58e26) 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 } ``` > [!NOTE] > > 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`. > [!NOTE] > > 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") { // ... } ``` > [!NOTE] > > `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. > **[Analyzing content](/content-moderation/submit-content)** > > 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. > **[Optimization](/content-moderation/optimizing-accuracy)** > > 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. > [!NOTE] > > 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. ![Content rules page showing four rules in evaluation order with the severity score triage fallback at the bottom](https://mintcdn.com/moderationapi/TAlbkkhuoVUU4PzM/images/rules-list.png?fit=max\&auto=format\&n=TAlbkkhuoVUU4PzM\&q=85\&s=1fb14b82186114dbccf165f8f73d2a3c) ### 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. > [!NOTE] > > 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 > [!WARNING] > > 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. ![Expanded rule card with two conditions joined by AND and a Review action](https://mintcdn.com/moderationapi/TAlbkkhuoVUU4PzM/images/rules-rule-card.png?fit=max\&auto=format\&n=TAlbkkhuoVUU4PzM\&q=85\&s=b3a5bd5d15fe9e5f0193e2aaffeab43a) ### 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. ![Simulation results showing Allowed, Reviewed, and Rejected counts with deltas after running draft rules against recent moderation history](https://mintcdn.com/moderationapi/TAlbkkhuoVUU4PzM/images/rules-simulate.png?fit=max\&auto=format\&n=TAlbkkhuoVUU4PzM\&q=85\&s=1b726b380bb0147eee61cf22d278e5de) *** ## 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). > [!NOTE] > > 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"; // Reads your API key from the MODAPI_SECRET_KEY environment variable 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. > [!TIP] > > 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", }, }); // result.recommendation.action is now at least "review", and // result.recommendation.reasonCodes 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). > [!NOTE] > > `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: > **[Text](/api-reference/moderate/analyze-text)** > > Analyze text content > **[Image](/api-reference/moderate/analyze-image)** > > Analyze image content > **[Object](/api-reference/moderate/analyze-object)** > > Analyze mixed content types > **[Video](/api-reference/moderate/analyze-video)** > > Analyze video content > **[Audio](/api-reference/moderate/analyze-audio)** > > 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, }); ``` > [!WARNING] > > Setting `doNotStore` to `true` will make parts of the moderation dashboard less useful, as content won't be available for review or analysis. > [!WARNING] > > 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. ![Severity score triage card with histogram and dual-handle slider](https://mintcdn.com/moderationapi/TAlbkkhuoVUU4PzM/images/severity-triage.png?fit=max\&auto=format\&n=TAlbkkhuoVUU4PzM\&q=85\&s=9c6217c33ae5370b0a6413ecf0e300f4) ### 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 | > [!TIP] > > 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. ![Policy threshold slider with histogram and would-flag / would-not-flag counts](https://mintcdn.com/moderationapi/TAlbkkhuoVUU4PzM/images/policy-threshold.png?fit=max\&auto=format\&n=TAlbkkhuoVUU4PzM\&q=85\&s=34c4691769926f208187f147603f76f5) ### 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 | ![Flagging tab showing 'Flag content when detected' and 'Shadow flag when detected' options for the Toxicity policy](https://mintcdn.com/moderationapi/5JRuVfuVCd9KlDpr/images/shadow-flag.png?fit=max\&auto=format\&n=5JRuVfuVCd9KlDpr\&q=85\&s=31c20e1eda14340a4bbb195f547dcbc9) 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: **1. Each policy scores the content** Every enabled policy returns a `probability`. Per-policy thresholds decide which policies set `flagged: true`. **2. Severity score is computed** Policies that scored contribute to the overall `severity_score`. The more severe the violation, the higher the score. **3. Content rules are evaluated** Your [rules](/content-moderation/rules) run top-to-bottom. The first match decides the recommended action. **4. Severity triage is the fallback** 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. > [!NOTE] > > 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. ![Correcting model decision](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/content-queue-correct.gif?s=716ecd765110f36e9786c2d171a7af7d) Correcting AI decision by removing a label and adding a new. 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. ![Illustration of blocks](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/illustration-1.jpg?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=ae8c3eafb7ddd4c10794ef1cf3ae0555) 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 > **[Guide to create a model](/guides/creating-a-custom-model)** > > Create a model in less than 15 minutes in the dashboard. ## How it works **1. Define labels** For example "Allowed" and "Blocked". **2. Add training data** Upload a CSV or use content you've already submitted to Moderation API. **3. Annotate data** If your training data is missing labels, you can annotate it using our annotation tools. **4. Begin training** Start the training and wait for it to finish. \*\*5. Use your new model \*\* 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/overview) 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](/policies/overview). ### Response signature Custom models have a similar response signature to the [pre-built classifiers](/policies/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. **label** `string` *required* The most probable label. This will always be the label with the highest score. Returns null if the analyzer fails. **score** `number` *required* The score of the label. From 0-1 score with 1 meaning a high probability of being correct. **label\_scores** `object` *required* 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. ![New project flow with steps for project name, domain, type, and review](https://mintcdn.com/moderationapi/O8C8LNLiFOm8YTLj/images/new-project.png?fit=max\&auto=format\&n=O8C8LNLiFOm8YTLj\&q=85\&s=9d7631afbeef69a450532c2e1280e86b) Creating a new project 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. ![Channel settings showing name, key, content types, and policies](https://mintcdn.com/moderationapi/O8C8LNLiFOm8YTLj/images/channel-settings.png?fit=max\&auto=format\&n=O8C8LNLiFOm8YTLj\&q=85\&s=db613d813200e6fa2add0eac9ade5d8c) A channel and its settings 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. ## Build with AI agents Let your coding agent add moderation to your app, or connect an agent to run your account. > **[Setup skill](/ai/skill)** > > Your coding agent adds Moderation API to your app > **[MCP server](/ai/mcp)** > > Triage queues, check authors and read analytics from an agent ## SDK quickstarts Learn how to get started with Moderation API in 5 minutes. > **[TypeScript quickstart](/quickstart/typescript)** > > Get started with TypeScript and npm > **[Python quickstart](/quickstart/python)** > > Get started with Python and pip > **[Go quickstart](/quickstart/go)** > > Get started with Go > **[Ruby quickstart](/quickstart/ruby)** > > Get started with Ruby > **[PHP quickstart](/quickstart/php)** > > Get started with PHP > **[Java quickstart](/quickstart/java)** > > Get started with Java > **[.NET quickstart](/quickstart/dotnet)** > > Get started with .NET ## Guides Can't find your use case? [Contact us](https://moderationapi.com/sales) and we'll help you get started. > **[Automate your moderation](/content-moderation)** > > Automate your moderation with the API. > **[Using review queues](/review/overview)** > > Learn how to use review queues to moderate content. > **[Guidelines](/policies/guidelines)** > > Define custom rules in natural language for your channel. > **[Pre-built models](/policies/overview)** > > Get an overview of the pre-built models available. > **[Creating a custom model](/custom-models)** > > Learn how to create a custom model in the dashboard. # Content moderation for chats Source: https://docs.moderationapi.com/guides/chat-content-moderation > [!NOTE] > > 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. ![Select your type of model](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/model-type-1.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=bc69146f95ee36e2095b0e6265336818) ## 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. ![Add your labels](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/model-labels-1.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=fdcaf38f9f09d24f6348536838ee69d8) 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.** ![Add your data](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/model-data-import-1.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=73e937b5b132869b4cb04753b958a28b) 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. ![Auto annotate your data](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/model-auto-annotation-1.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=fde60e612521902ed09987a546523aee) 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. ![Manually annotate your data](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/model-manual-annotation-1.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=b18c9f13bf7b5536bf4aeafd5e265033) 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. ![Start the training](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/model-training-1.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=3aad9b43b6c66b8d66ec9621e3c1eba5) ### 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. ![Test your model](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/model-test-run-1.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=bcf07ecf5029735aca5c7bef3ca4ed24) ## 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 > [!NOTE] > > This page is under construction. \\ > > Please reach out at [support@moderationapi.com](mailto:support@moderationapi.com) to get help with this feature. # Migrate from Perspective API Source: https://docs.moderationapi.com/guides/migrate-from-perspective-api Move an existing Perspective API integration to Moderation API: swap the request, map the response fields, and map each Perspective attribute to a policy. Perspective API gives you toxicity scores you threshold in your own code. Moderation API returns the same scores for the same categories, so that part of your integration carries over. It also runs many more policies, accepts images, video, and audio, and includes a review queue, a rules engine, and custom models. None of that is needed to migrate. This guide shows how to replace the Perspective call, where each field and attribute goes, and how to carry your thresholds across. > [!NOTE] > > Google Jigsaw has announced that Perspective API will no longer be in service > after 2026. Existing integrations keep working until then, but new usage and > quota increases are no longer approved. > [!TIP] > > You don't have to do this yourself. Send us the code that calls Perspective > and we'll write the migration for you, validate the scores against your > historical data, and stay on call through cutover. [Book a migration > call](https://moderationapi.com/sales) or email > [support@moderationapi.com](mailto:support@moderationapi.com). ## Hand it to a coding agent This guide is available as Markdown, so a coding agent working in your repository can do the migration for you. Paste the prompt below into Claude Code, Cursor, Codex, or whatever agent you use, and it will read the guide and rewrite your Perspective call sites. ```text Prompt wrap theme={"theme":"nord"} Migrate my Perspective API integration to Moderation API. Read the guide at https://docs.moderationapi.com/guides/migrate-from-perspective-api.md and follow it: swap the request, map the response fields, and map each Perspective attribute to the policy in the guide's table. Keep my existing threshold logic by reading probability from the mapped policies, and don't change behavior beyond the API swap. Tell me about anything in my code that doesn't map cleanly, such as attributes or fields the guide marks as unsupported. ``` Use the **Copy page** option at the top of this page if your agent can't fetch URLs. *** ## At a glance | | Perspective API | Moderation API | | - | - | - | | Endpoint | `POST https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze` | `POST https://api.moderationapi.com/v1/moderate` | | Authentication | API key in the `?key=` query parameter | `Authorization: Bearer ` header | | What you request | Attributes, per request (`requestedAttributes`) | Policies, per channel (configured in the dashboard) | | Scores | `attributeScores..summaryScore.value` (0 to 1) | `policies[].probability` (0 to 1), plus `flagged` per policy | | Decision | You threshold the score yourself | `recommendation.action` (`allow`, `review`, `reject`), or threshold `probability` yourself | | Conversation context | Sent inline in `context.entries` | Retrieved from earlier submissions with the same `conversationId` | | Language | Optional `languages` hint | Detected automatically, returned in `insights` | | Content types | Text | Text, image, video, audio, and mixed objects | *** ## Before you start Create a project in the [dashboard](https://dash.moderationapi.com) and copy its API key. Store it as `MODAPI_SECRET_KEY`. The TypeScript and Python SDKs read this environment variable. The PHP and Ruby examples below read it and pass it to the client. See [Authentication](/api-reference/authentication) for details. ### Enable the policies that match your attributes With Perspective you list the attributes you want in every request. With Moderation API the policies are configured once, on the project's channel, and every request runs all of them. Open **Policies** in the dashboard and enable the policies that correspond to the attributes you request today. Use the [attribute mapping table](#map-attributes-to-policies) below to find each one. *** ## Swap the request Replace the Perspective call with a call to `/moderate`. Pick your language below, then switch between the **Perspective** and **Moderation API** tabs to compare the two calls. **TypeScript** ```typescript Perspective theme={"theme":"nord"} import { google } from "googleapis"; const client = await google.discoverAPI( "https://commentanalyzer.googleapis.com/$discovery/rest?version=v1alpha1" ); const { data } = await client.comments.analyze({ key: process.env.PERSPECTIVE_API_KEY, resource: { comment: { text: "You are an idiot" }, requestedAttributes: { TOXICITY: {}, SEVERE_TOXICITY: {}, INSULT: {}, THREAT: {}, IDENTITY_ATTACK: {}, }, clientToken: "comment-123", doNotStore: true, }, }); const toxicity = data.attributeScores.TOXICITY.summaryScore.value; ``` ```typescript Moderation API theme={"theme":"nord"} import { ModerationApi } from "@moderation-api/sdk"; // Reads MODAPI_SECRET_KEY from the environment const moderationApi = new ModerationApi(); const result = await moderationApi.content.submit({ content: { type: "text", text: "You are an idiot" }, contentId: "comment-123", doNotStore: true, }); let toxicity: number | undefined; for (const policy of result.policies) { if (policy.type === "classifier" && policy.id === "toxicity") { toxicity = policy.probability; } } // Or skip thresholds entirely if (result.recommendation.action === "reject") { // Block the content } ``` **Python** ```python Perspective theme={"theme":"nord"} import os from googleapiclient import discovery client = discovery.build( "commentanalyzer", "v1alpha1", developerKey=os.environ["PERSPECTIVE_API_KEY"], discoveryServiceUrl=( "https://commentanalyzer.googleapis.com/" "$discovery/rest?version=v1alpha1" ), static_discovery=False, ) response = client.comments().analyze(body={ "comment": {"text": "You are an idiot"}, "requestedAttributes": { "TOXICITY": {}, "SEVERE_TOXICITY": {}, "INSULT": {}, "THREAT": {}, "IDENTITY_ATTACK": {}, }, "clientToken": "comment-123", "doNotStore": True, }).execute() toxicity = ( response["attributeScores"]["TOXICITY"]["summaryScore"]["value"] ) ``` ```python Moderation API theme={"theme":"nord"} import os from moderation_api import ModerationAPI client = ModerationAPI( secret_key=os.environ["MODAPI_SECRET_KEY"], ) response = client.content.submit( content={"type": "text", "text": "You are an idiot"}, content_id="comment-123", do_not_store=True, ) toxicity = next( (p.probability for p in response.policies if p.id == "toxicity"), None, ) # Or skip thresholds entirely if response.recommendation.action == "reject": pass # Block the content ``` **Ruby** ```ruby Perspective theme={"theme":"nord"} require "net/http" require "json" uri = URI( "https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze" \ "?key=#{ENV['PERSPECTIVE_API_KEY']}" ) response = Net::HTTP.post( uri, { comment: { text: "You are an idiot" }, requestedAttributes: { TOXICITY: {}, SEVERE_TOXICITY: {}, INSULT: {}, THREAT: {}, IDENTITY_ATTACK: {}, }, clientToken: "comment-123", doNotStore: true, }.to_json, "Content-Type" => "application/json", ) data = JSON.parse(response.body) toxicity = data["attributeScores"]["TOXICITY"]["summaryScore"]["value"] ``` ```ruby Moderation API theme={"theme":"nord"} require "moderation_api" Operations = ModerationApi::Models::Operations client = ModerationApi::ModerationApi.new( secret_key: ENV.fetch("MODAPI_SECRET_KEY"), ) response = client.content.submit(body: Operations::SubmitContentRequestBody.new( content: Operations::TextRequest.new(text: "You are an idiot"), content_id: "comment-123", do_not_store: true, )) result = response.object toxicity = result.policies .find { |p| p.id == "toxicity" } &.probability # Or skip thresholds entirely if result.recommendation.action.serialize == "reject" # Block the content end ``` **PHP** ```php Perspective theme={"theme":"nord"} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode([ "comment" => ["text" => "You are an idiot"], "requestedAttributes" => [ "TOXICITY" => (object) [], "SEVERE_TOXICITY" => (object) [], "INSULT" => (object) [], "THREAT" => (object) [], "IDENTITY_ATTACK" => (object) [], ], "clientToken" => "comment-123", "doNotStore" => true, ]), ]); $data = json_decode(curl_exec($ch), true); $toxicity = $data["attributeScores"]["TOXICITY"]["summaryScore"]["value"]; ``` ```php Moderation API theme={"theme":"nord"} setSecurity(getenv("MODAPI_SECRET_KEY")) ->build(); $response = $sdk->content->submit(new Operations\SubmitContentRequestBody( content: new Operations\TextRequest(text: "You are an idiot"), contentId: "comment-123", doNotStore: true, )); $result = $response->object; $toxicity = null; foreach ($result->policies as $policy) { if ($policy->id === "toxicity") { $toxicity = $policy->probability; } } // Or skip thresholds entirely if ($result->recommendation->action === Operations\SubmitContentRecommendationAction::Reject) { // Block the content } ``` **cURL** ```bash Perspective theme={"theme":"nord"} curl -X POST \ "https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze?key=$PERSPECTIVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "comment": { "text": "You are an idiot" }, "requestedAttributes": { "TOXICITY": {}, "SEVERE_TOXICITY": {}, "INSULT": {}, "THREAT": {}, "IDENTITY_ATTACK": {} }, "clientToken": "comment-123", "doNotStore": true }' ``` ```bash Moderation API theme={"theme":"nord"} curl -X POST https://api.moderationapi.com/v1/moderate \ -H "Authorization: Bearer $MODAPI_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "content": { "type": "text", "text": "You are an idiot" }, "contentId": "comment-123", "doNotStore": true }' ``` > [!TIP] > > Rather not touch the call sites yourself? [Send us your > code](https://moderationapi.com/sales) and we'll write the migration for you. *** ## Read the response Perspective returns one score per requested attribute. Moderation API returns one entry per enabled policy in the `policies` array, plus an overall `evaluation` and a `recommendation` you can act on directly. ```json Perspective response theme={"theme":"nord"} { "attributeScores": { "TOXICITY": { "summaryScore": { "value": 0.91, "type": "PROBABILITY" } }, "SEVERE_TOXICITY": { "summaryScore": { "value": 0.42, "type": "PROBABILITY" } } }, "languages": ["en"], "detectedLanguages": ["en"], "clientToken": "comment-123" } ``` ```json Moderation API response (trimmed) theme={"theme":"nord"} { "content": { "id": "comment-123" }, "evaluation": { "flagged": true, "flag_probability": 0.91, "severity_score": 0.62 }, "recommendation": { "action": "review", "reason_codes": ["severity_review"] }, "policies": [ { "id": "toxicity", "type": "classifier", "probability": 0.91, "flagged": true }, { "id": "toxicity_severe", "type": "classifier", "probability": 0.42, "flagged": false } ], "insights": [ { "id": "language", "type": "insight", "value": "en", "probability": 0.99 } ] } ``` See [Understanding API responses](/content-moderation/acting-on-responses) for the full response shape, and the [field mapping](#map-request-and-response-fields) below for every field. *** ## If you rely on the scores Most Perspective integrations read `summaryScore.value` for each attribute and compare it against a threshold in their own code, often a different one per attribute. You can keep doing exactly that. Each policy in the `policies` array carries a `probability` between 0 and 1, the same range Perspective uses. Build the same score object you have today from the mapped policies, and your downstream logic doesn't need to change: ```javascript Perspective theme={"theme":"nord"} const scores = data.attributeScores; const toxicity = scores.TOXICITY.summaryScore.value; const severe = scores.SEVERE_TOXICITY.summaryScore.value; const identity = scores.IDENTITY_ATTACK.summaryScore.value; if (severe >= 0.9 || identity >= 0.8) { reject(); } else if (toxicity >= 0.7) { review(); } ``` ```javascript Moderation API theme={"theme":"nord"} const score = (id) => result.policies.find((p) => p.id === id)?.probability ?? 0; const toxicity = score("toxicity"); const severe = score("toxicity_severe"); const identity = score("hate"); if (severe >= 0.9 || identity >= 0.8) { reject(); } else if (toxicity >= 0.7) { review(); } ``` > [!WARNING] > > The range is the same, but the models are not. A comment Perspective scored > `0.72` for `TOXICITY` won't necessarily score `0.72` for `toxicity`, so treat > the thresholds you tuned for Perspective as a starting point. Run both APIs > on the same traffic before you cut over. See [Validate before you cut > over](#validate-before-you-cut-over). > [!NOTE] > > `probability` is always returned, even below the threshold. Perspective's > `scoreThreshold` option, which drops scores under a cutoff, has no > equivalent. Filter on your side if you need it. ### Use the dashboard to find the right thresholds You can set threshold values in the dashboard and see what they would do before committing to them. Each policy has a **Threshold** tab under **Policies** in your project, showing a histogram of the confidence scores from your recent traffic. As you move the slider, the bars to the right are what would be flagged and the bars to the left what wouldn't, with counts for how many recent items would flip sides. ![Policy threshold slider with histogram and would-flag / would-not-flag counts](https://mintcdn.com/moderationapi/TAlbkkhuoVUU4PzM/images/policy-threshold.png?fit=max\&auto=format\&n=TAlbkkhuoVUU4PzM\&q=85\&s=34c4691769926f208187f147603f76f5) This works well as a calibration tool even if you keep your thresholds in code: send real traffic, find the cutoff on the histogram that matches the behavior you had with Perspective, then copy that number into your integration. See [Per-policy thresholds](/content-moderation/thresholds#per-policy-thresholds). > [!TIP] > > We can also calibrate the thresholds for you. Share a sample of content with > your Perspective scores or your moderators' decisions, and we'll run the > comparison against Moderation API and hand back the threshold per policy that > best reproduces your current behavior. [Get in > touch](https://moderationapi.com/sales) to set it up. ### Or let the API make the decision Once the thresholds live in the dashboard, you can drop the score comparison entirely. The API sets `flagged` on each policy, combines everything into a `severity_score`, and returns a `recommendation.action` of `allow`, `review`, or `reject`. ```javascript theme={"theme":"nord"} switch (result.recommendation.action) { case "reject": // Block the content break; case "review": // Save it and send it to the review queue break; case "allow": // Publish break; } ``` Thresholds become something you tune against real data instead of a constant in your code, and [content rules](/content-moderation/rules) cover the cases a single score can't express, like "reject `toxicity_severe` from new accounts, review it from everyone else." *** ## Validate before you cut over Run both APIs side by side on live traffic before you remove Perspective: 1. Enable **dry-run** in your project settings. The API still analyzes and stores everything, but `flagged` is always `false`, so nothing is blocked while you compare. 2. Send your production content to `/moderate` alongside your existing Perspective call and compare scores on the content you already have decisions for. 3. Adjust per-policy thresholds using the histogram on each policy's **Threshold** tab, then turn dry-run off. If you'd rather test one policy at a time, [shadow flagging](/content-moderation/thresholds#flag-vs-shadow-flag) lets a policy score traffic without affecting `flagged`. *** ## Map request and response fields ### Request | Perspective API | Moderation API | Notes | | - | - | - | | `comment.text` | `content.text` with `content.type: "text"` | Up to 100,000 characters per text. | | `comment.type` (`PLAIN_TEXT`, `HTML`) | — | Send plain text. | | `context.entries[]` | `conversationId` + [context awareness](/content-moderation/submit-content#context-awareness) | Perspective takes prior messages inline. Moderation API looks up earlier submissions with the same `conversationId` (or `authorId`), so submit each message as it arrives. | | `requestedAttributes` | Channel policies | Enabled per channel in the dashboard, not per request. See the [attribute mapping](#map-attributes-to-policies). Enterprise plans can override channel policies per request with the `policies` field. | | `requestedAttributes..scoreThreshold` | Per-policy [detection threshold](/content-moderation/thresholds#per-policy-thresholds) | Perspective omits scores below the threshold; Moderation API always returns `probability` and sets `flagged` when it crosses the threshold. | | `requestedAttributes..scoreType` | — | Scores are always probabilities between 0 and 1. | | `languages` | — | Language is detected automatically and returned as the `language` insight. | | `doNotStore` | `doNotStore` | Same name. Content is analyzed but not stored, so it won't appear in the dashboard or review. | | `clientToken` | `contentId` | Echoed back as `content.id`. Resubmitting the same `contentId` updates the stored item instead of creating a duplicate. | | `sessionId` | `conversationId` or `authorId` | Use `conversationId` to group a thread, `authorId` to group by user. Both power context awareness and the review queue. | | `communityId` | `channel` | Route the request to a specific channel configuration. Defaults to the project's default channel. | | `spanAnnotations` | WIP | Classifier policies don't return per-span scores yet. Entity matchers such as `personal_information` and `url` do return `span` offsets for each match. | | — | `metaType`, `metadata`, `timestamp`, `clientAction` | New. See [Submitting content](/content-moderation/submit-content#content-metadata). | ### Response | Perspective API | Moderation API | Notes | | - | - | - | | `attributeScores..summaryScore.value` | `policies[].probability` where `policies[].id` is the mapped policy | Both are probabilities between 0 and 1. | | `attributeScores..summaryScore.type` | — | Always a probability. | | `attributeScores..spanScores[]` | WIP | See `spanAnnotations` above. | | — | `policies[].flagged` | New. `true` when `probability` crossed the policy's threshold. | | — | `evaluation.flagged`, `evaluation.severity_score` | New. Whether any policy flagged, and a single 0 to 1 severity across all of them. | | — | `recommendation.action`, `recommendation.reason_codes` | New. The suggested action and why. See [Use the recommendation](/content-moderation/acting-on-responses#use-the-recommendation). | | `languages`, `detectedLanguages` | `insights[]` entry with `id: "language"` | The detected language code is in `value`. | | `clientToken` | `content.id` | The `contentId` you sent, or a generated ID if you didn't send one. | ### Sending feedback Perspective's `comments:suggestscore` method lets you report a score you disagree with. Moderation API has no equivalent call. Instead, decisions your moderators make in the [review queue](/review/overview) are stored as cases in the [casebook](/casebook/overview), which applies those rulings to similar content automatically. You can resolve items programmatically with the [resolve a queue item](/api-reference/review-queues/resolve-a-queue-item) endpoint. *** ## Map attributes to policies Each Perspective attribute maps to one Moderation API policy. Enable the policy in the dashboard, then read its entry in the `policies` array using the `id` below. | Perspective attribute | Moderation API policy | Status | Notes | | - | - | :-: | - | | `TOXICITY` | [`toxicity`](/policies/toxicity) | Supported | Rude, disrespectful, or hostile language. | | `SEVERE_TOXICITY` | [`toxicity_severe`](/policies/toxicity) | Supported | A stricter classifier for abusive content. | | `IDENTITY_ATTACK` | [`hate`](/policies/toxicity) | Supported | Hate and discrimination targeting protected groups. Also runs on images and video. | | `INSULT` | [`toxicity`](/policies/toxicity) | Supported | Insults are scored by the `toxicity` policy; there is no separate insult policy. | | `PROFANITY` | [`profanity`](/policies/nsfw) | Supported | Swearing and vulgar language. | | `THREAT` | [`violence`](/policies/nsfw) | Supported | Threats and violent content. Also runs on images and video. | | `SEXUALLY_EXPLICIT` | [`sexual`](/policies/nsfw) | Supported | Also runs on images, video, and audio. | | `FLIRTATION` | [`flirtation`](/policies/nsfw) | Supported | Pickup lines, compliments on looks, innuendo. | ### Bridging attributes Perspective's experimental bridging attributes score constructive qualities rather than harms. Moderation API doesn't have equivalents yet. | Perspective attribute | Moderation API policy | Status | Notes | | - | - | :-: | - | | `AFFINITY_EXPERIMENTAL` | — | WIP | Shared interests or outlook between the author and others. | | `COMPASSION_EXPERIMENTAL` | — | WIP | Concern, empathy, or support for others. | | `CURIOSITY_EXPERIMENTAL` | — | WIP | Follow-up questions to understand another person or idea. | | `NUANCE_EXPERIMENTAL` | — | WIP | Multiple points of view or useful context. | | `PERSONAL_STORY_EXPERIMENTAL` | — | WIP | A personal experience used to support the comment. | | `REASONING_EXPERIMENTAL` | — | WIP | Specific, well-reasoned points without provocation. | | `RESPECT_EXPERIMENTAL` | — | WIP | Deference or appreciation toward others. | > [!NOTE] > > If you rely on any attribute marked WIP, [let us > know](mailto:support@moderationapi.com). It helps us prioritize. Once you've migrated, consider enabling policies Perspective never offered: > **[Self-harm](/policies/nsfw)** > > Self-harm, suicide, and eating-disorder content, which often deserves a > support resource rather than a rejection. > **[Personal information](/policies/privacy)** > > Detect and mask emails, phone numbers, and addresses. > **[Phishing and code abuse](/policies/spam)** > > Scam messages, prompt injection, and malicious code. > **[Guidelines](/policies/guidelines)** > > Write your own rules in plain language and get them back as policies. *** ## Other differences worth knowing * **Rate limits.** Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers, and a `429` includes `Retry-After`. See [Rate limits](/api-reference/rate-limits). * **Partial failures.** If one policy fails, the rest still return. Check `meta.status` for `partial_success` and read the `errors` array. See [Handle errors](/content-moderation/acting-on-responses#handle-errors). * **Mixed content.** Send a profile or listing with several fields as a single `object` and get `flagged_fields` back per policy. See [Object](/content-moderation/submit-content#object). * **Review queue.** Every stored submission is available for human review without any extra integration work. See [Review](/review/overview). # Designing a moderation workflow Source: https://docs.moderationapi.com/guides/moderation-workflow > [!NOTE] > > 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 > [!NOTE] > > 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 [YouTube video player](https://www.youtube.com/embed/l_zjaWW7weA) ## 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. ![User Report Flow](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/guides/images/report-user-flow.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=2a8ba50b0004c9b45c0afd59a061cc47) *** ## 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. ![Creating a New Project](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/guides/images/create-project.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=ce23e7f98c55f8ef01aadce67d3c16d5) ### 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. ![Creating a New Action](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/guides/images/create-action.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=c0a459cb3bc7cc30bbc0ff25d2d4963d) ### 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*. ![Filter bar with filter chips and the Save dropdown used to create a view](https://mintcdn.com/moderationapi/rnXd3ZotQdidNR7k/images/queue-filter-bar.png?fit=max\&auto=format\&n=rnXd3ZotQdidNR7k\&q=85\&s=4be91f981a071aefcf04d327ba6c2a26) Active filters are visible as chips, and Save persists them to a view ### 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 and your webhook secret. Find both under [API Keys in the dashboard](https://dash.moderationapi.com/project/latest/configure/api-keys). ```bash theme={"theme":"nord"} MODAPI_SECRET_KEY=your_project_api_key MODAPI_WEBHOOK_SECRET=your_webhook_secret ``` #### 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"; // Reads the API key from the MODAPI_SECRET_KEY environment variable export const moderationApi = new ModerationApi(); // Or pass the key explicitly // export const moderationApi = new ModerationApi({ // secretKey: process.env.MY_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 { success } = await moderationApi.actions.execute({ actionKey: "report_profile", 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 uses the SDK's `validateWebhook` to verify the `modapi-signature` header with `MODAPI_WEBHOOK_SECRET`, then dispatches on the event `type`. ```javascript /pages/api/webhooks.js theme={"theme":"nord"} import { validateWebhook } from "@moderation-api/sdk"; import db from "../../lib/db"; const handler = async (req, res) => { let event; try { // Reads the raw body from the request and checks the signature event = await validateWebhook({ request: req }); } catch { return res.status(401).send(); } // `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. ![Queue Overview](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/guides/images/queue-profiles.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=70937f22f316d6ddc688eead20a55437) ### 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. ![Queue Filter](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/guides/images/queue-filter.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=f83a3a04b1b9bcc6b7133306be14848b) 2. After setting the filter, you'll see items labeled as `UNSAFE`. ![Filtered Queue with Unsafe Labels](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/guides/images/queue-filter-unsafe.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=9478316364375203378f92ceb1631136) ### Removing a Profile 1. Click on a queue item to open its detail view, where you can see the profile content and metadata. ![Unsafe Item Detail](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/guides/images/unsafe-item-detail.png?fit=max\&auto=format\&n=n7BcGOdXBzdto1Xk\&q=85\&s=ae64569a7dea958d7e253f3520987deb) 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 > **[Pre-built models](/policies/overview)** > > These are pre-built models that can be used to analyze content without any > additional training. > **[Custom models](/custom-models/overview)** > > These are models that are trained on user-provided data to analyze content > specific to their use case. > **[Wordlists](/policies/wordlists)** > > 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. } ``` > [!TIP] > > 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. } ``` > [!NOTE] > > `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 > **[Guidelines](/policies/guidelines)** > > Define custom rules in natural language. Each guideline is evaluated by an LLM and returned as its own policy. > **[Wordlists](/policies/wordlists)** > > Your own lists of words and phrases, matched with semantic understanding. Block them, require them, or just extract them. > **[Privacy](/policies/privacy)** > > Personal information detection, intent to share contact details, PII masking, and face detection in images. > **[Toxicity & Hate](/policies/toxicity)** > > Toxicity, severe toxicity, and hate-based content including discrimination and extremism. > **[NSFW](/policies/nsfw)** > > Sexual content, flirtation, profanity, violence, and self-harm across text, image, video, and audio. > **[Illicit & Regulated](/policies/illicit)** > > Drugs, alcohol, firearms, gambling, adult products, cannabis, crypto, and other regulated categories. > **[Spam & Security](/policies/spam)** > > Spam, low-quality content, self-promotion, code abuse, phishing, and URL extraction. > **[URL Risk](/policies/url-risk)** > > Real-time risk scoring for URLs — phishing, malware, brand impersonation, credential harvesting. > **[Unicode spoofing](/policies/unicode-spoofing)** > > Lookalike characters, mixed alphabets, invisible characters, and Zalgo used to disguise banned words. > **[Topics](/policies/topics)** > > Political and religious content for platforms that want to keep discussions on-topic. > [!NOTE] > > 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 (In active development. Behavior and accuracy may change.) | | `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. > [!NOTE] > > 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 (In active development. Behavior and accuracy may change.) | | `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 (Available as 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 | > [!TIP] > > 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)); ``` > [!NOTE] > > `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); } ``` > [!TIP] > > 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. > [!NOTE] > > 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 | > [!TIP] > > 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. **1. Open the policy** In your channel, go to **Policies → Spam** and find the **Unicode spoofing** card. **2. Enable detection** Toggle **Enable detection** on. **3. Choose how to act** 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. **4. Pick which signals enforce (optional)** 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. ![Item detail view with three highlighted words. A tooltip over the first reads 'Mixed-script words', 'Scripts: Latin + Cyrillic', 'Looks like: fucking', and lists the suspicious characters U+0441 and U+043A.](https://mintcdn.com/moderationapi/U0KIcHAt9YjI_EfS/images/unicode-spoofing-evidence.png?fit=max\&auto=format\&n=U0KIcHAt9YjI_EfS\&q=85\&s=361293c185b2a8c4c869dceaa62c3b55) 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. > [!NOTE] > > 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. **flagged** `boolean` Whether an enabled signal fired. **probability** `number` `1` when an enabled signal fired, `0` otherwise. In shadow flag mode this is still `1` while `flagged` stays `false`. **labels** `array` 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. **flagged\_fields** `string[]` For object content, the field keys where a signal fired. Omitted for plain text. **data** `object` **properties** **counts.words\_total** `number` Total words analyzed. **counts.words\_affected** `number` How many words carried a signal. **sample\_words** `string[]` 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. > [!NOTE] > > 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. > [!TIP] > > 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: **evaluation.unicode\_spoofed** `boolean` `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. **content.modified** `string | object` 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 > **Will this flag legitimate non-English messages?** > > 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. > **Does it add usage or latency?** > > No extra usage — Unicode spoofing detection is included, with no additional > model call, and adds negligible latency to a moderation request. > **Which alphabets and characters are covered?** > > 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. > **Is the detection kept up to date?** > > 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. | > [!NOTE] > > 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. > [!NOTE] > > 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 > **Why does the score for the same URL change over time?** > > 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. > **What threshold should I use?** > > `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. > **A legitimate URL of mine is being flagged. What do I do?** > > 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 ![Wordlist thresholds](https://mintcdn.com/moderationapi/O8C8LNLiFOm8YTLj/images/wordlist-threshold.png?fit=max\&auto=format\&n=O8C8LNLiFOm8YTLj\&q=85\&s=f7dcfa3fa995891bbb4621134cdac53b) 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. > [!NOTE] > > **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 the CLI Source: https://docs.moderationapi.com/quickstart/cli Learn how to moderate content from your terminal, scripts and CI with the Moderation API CLI *** *** ## 1. Install the CLI ```bash Homebrew theme={"theme":"nord"} brew install moderation-api/tap/modapi ``` ```bash macOS / Linux theme={"theme":"nord"} curl -fsSL https://raw.githubusercontent.com/moderation-api/cli/main/scripts/install.sh | bash ``` ```powershell Windows theme={"theme":"nord"} iwr -useb https://raw.githubusercontent.com/moderation-api/cli/main/scripts/install.ps1 | iex ``` ```bash Go theme={"theme":"nord"} go install github.com/moderation-api/cli/cmd/modapi@latest ``` > [!NOTE] > > You can also download a binary for your platform from the [releases page](https://github.com/moderation-api/cli/releases). ## 2. Set up your project Sign in with your Moderation API account, then run `init` in your project's directory: ```bash theme={"theme":"nord"} modapi login # once per computer; opens the browser modapi init # in your project's directory ``` `login` opens the browser, where you sign in and pick an organization. On a computer without a browser, such as over SSH, use `modapi login --device` and confirm a code on another device. When your organization already has projects, `init` asks whether to link one of them or create a new one. It writes the project's keys to your env file: `MODAPI_SECRET_KEY` and `MODAPI_WEBHOOK_SECRET`, in `.env.local` for Next.js or `.env` otherwise. It also adds that file to `.gitignore`. Describe your platform, and the project suggests moderation policies for it: ```bash theme={"theme":"nord"} modapi init --domain acme.com \ --context "Marketplace where buyers and sellers chat before a booking" \ --brief "Stop contact details shared outside the platform, scams and harassment" ``` If the env file already has a working `MODAPI_SECRET_KEY`, `init` changes nothing. Add `--dry-run` to see what it would do. To connect a directory to a project you already have, run `modapi link`, or name the project: ```bash theme={"theme":"nord"} modapi link "Acme Marketplace" ``` Linking makes a new key for the project, named after your computer and directory, so you can revoke it on its own. In scripts and coding agents, pass `init --project ` to link or `init --new` to create, instead of answering the question. Check the setup at any time with `modapi doctor`. It checks the key against the API, the webhook secret, `.gitignore` and the SDK dependency, and says how to fix each problem it finds. > [!TIP] > > Already have a project? Set its API key in the `MODAPI_SECRET_KEY` environment variable instead, or run `modapi configure` to store it in your operating system's keychain. In CI, skip `login` and set `MODAPI_ORGANIZATION_KEY` to an [organization key](/api-reference/management-api). ### Let your coding agent add moderation `init` does not change your code. To add the moderation calls, install the setup skill for your coding agent: ```bash theme={"theme":"nord"} npx skills add moderation-api/skills ``` Then ask your agent to "set up Moderation API". The skill finds where users create content, adds the moderation call, handles the result and sets up webhooks. It asks you before each change. See [Setup skill](/ai/skill). ## 3. Submit content Use `modapi content submit` to moderate text, images, video, audio, or objects with several fields. Only `--content` is required. ```bash Text moderation theme={"theme":"nord"} modapi content submit \ --content '{"type":"text","text":"Hello world!"}' \ --content-id message-123 \ --author-id user-123 \ --conversation-id room-456 \ --metadata '{"customField":"value"}' ``` ```bash Image moderation theme={"theme":"nord"} modapi content submit \ --content '{"type":"image","url":"https://example.com/image.jpg"}' \ --content-id image-456 \ --author-id user-123 ``` ```bash Object moderation theme={"theme":"nord"} modapi 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"}}}' \ --content-id post-789 \ --author-id user-123 ``` ## 4. Use the result Use `--jq` to read one field of the result. For example, get the recommended action (`allow`, `review` or `reject`). Add `--raw-output` to print the value as plain text: ```bash theme={"theme":"nord"} action=$(modapi content submit \ --content '{"type":"text","text":"Hello world!"}' \ --jq '.recommendation.action' --raw-output) if [ "$action" = "reject" ]; then echo "Content rejected" fi ``` For the full result as JSON, use `--output-format json`. ## 5. Preview a request Add `--dry-run` to any command to see the request it would send. The CLI does not send the request, so it does not use your quota: ```bash theme={"theme":"nord"} modapi content submit --content '{"type":"text","text":"Hello world!"}' --dry-run ``` ## 6. Explore other commands The CLI has a command for each API endpoint. Some examples: ```bash theme={"theme":"nord"} # List authors modapi authors list # Get every page of moderation actions as JSON, one item per line modapi actions list --all --output-format json # Get the webhook signing secret modapi webhook-secret retrieve # Show all commands, or the flags of one command modapi --help modapi content submit --help ``` If a command fails, it exits with a code that is not `0`: `1` for API errors, `2` for usage errors and `3` for authentication errors. ### Organization key Commands for projects and channels (`modapi projects` and `modapi channels`) use the [management API](/api-reference/management-api) and need an organization key. To run them while `MODAPI_SECRET_KEY` holds your project key, pass the organization key with `--secret-key`: ```bash theme={"theme":"nord"} modapi projects list --secret-key "$MODAPI_ORGANIZATION_KEY" ``` An organization key can also run the other commands for a project: name it with `--project-id`. ```bash theme={"theme":"nord"} modapi authors list --secret-key "$MODAPI_ORGANIZATION_KEY" --project-id "$PROJECT_ID" ``` *** *** *** # 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 ``` > [!NOTE] > > Requires PHP 8.2 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. The SDK returns a response object. The moderation result is in `$response->object`. ```php Text moderation theme={"theme":"nord"} setSecurity(getenv('MODAPI_SECRET_KEY')) ->build(); // Submit text for moderation $response = $sdk->content->submit(new Operations\SubmitContentRequestBody( content: new Operations\TextRequest(text: 'Hello world!'), contentId: 'message-123', authorId: 'user-123', conversationId: 'room-456', metadata: ['customField' => 'value'], )); $result = $response->object; // Check if content was flagged if ($result->evaluation->flagged) { echo "Content was flagged\n"; } // Act on the recommendation switch ($result->recommendation->action) { case SubmitContentRecommendationAction::Allow: echo "Content is safe.\n"; // Save to database or proceed... break; case SubmitContentRecommendationAction::Review: echo "Content needs review\n"; // Send to review queue break; case SubmitContentRecommendationAction::Reject: echo "Content rejected\n"; // Block the content, show an error, etc... break; } ``` ```php Image moderation theme={"theme":"nord"} setSecurity(getenv('MODAPI_SECRET_KEY')) ->build(); // Submit image for moderation $response = $sdk->content->submit(new Operations\SubmitContentRequestBody( content: new Operations\ImageRequest(url: 'https://example.com/image.jpg'), contentId: 'image-456', authorId: 'user-123', conversationId: 'room-456', metadata: ['customField' => 'value'], )); $result = $response->object; // Check if content was flagged if ($result->evaluation->flagged) { echo "Image was flagged\n"; } // Act on the recommendation switch ($result->recommendation->action) { case SubmitContentRecommendationAction::Allow: echo "Image is safe.\n"; // Save to database or proceed... break; case SubmitContentRecommendationAction::Review: echo "Image needs review\n"; // Send to review queue break; case SubmitContentRecommendationAction::Reject: echo "Image rejected\n"; // Block or require review break; } ``` ```php Object moderation theme={"theme":"nord"} setSecurity(getenv('MODAPI_SECRET_KEY')) ->build(); // Submit object for moderation $response = $sdk->content->submit(new Operations\SubmitContentRequestBody( content: new Operations\ObjectT(data: [ 'name' => new Operations\DataText(text: 'John Doe'), 'email' => new Operations\DataText(text: 'john.doe@example.com'), 'picture' => new Operations\DataImage(url: 'https://example.com/image.jpg'), ]), contentId: 'post-789', authorId: 'user-123', conversationId: 'room-456', metadata: ['customField' => 'value'], )); $result = $response->object; // Check if content was flagged if ($result->evaluation->flagged) { echo "Object was flagged\n"; } // Act on the recommendation switch ($result->recommendation->action) { case SubmitContentRecommendationAction::Allow: echo "Object is safe.\n"; // Save to database or proceed... break; case SubmitContentRecommendationAction::Review: echo "Object needs review\n"; // Send to review queue break; case SubmitContentRecommendationAction::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"} setSecurity(getenv('MODAPI_SECRET_KEY')) ->build(); try { $response = $sdk->content->submit(new Operations\SubmitContentRequestBody( content: new Operations\TextRequest(text: 'Hello world!'), )); echo "Recommendation: " . $response->object->recommendation->action->value . "\n"; } catch (Errors\BadRequestErrorThrowable $e) { echo "Invalid request: " . $e->getMessage(); } catch (Errors\AuthenticationErrorThrowable $e) { echo "Authentication failed: " . $e->getMessage(); } catch (Errors\RateLimitErrorThrowable $e) { echo "Rate limit exceeded: " . $e->getMessage(); } catch (Errors\APIException $e) { echo "API error " . $e->statusCode . ": " . $e->getMessage(); } ``` *** *** *** # 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 pip theme={"theme":"nord"} pip install moderation_api ``` ```bash uv theme={"theme":"nord"} uv add moderation_api ``` ```bash poetry theme={"theme":"nord"} poetry add moderation_api ``` > [!NOTE] > > Requires Python 3.10 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. ```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["MODAPI_SECRET_KEY"]) # Submit text for moderation response = client.content.submit( content={ "type": "text", "text": "Hello world!" }, content_id="message-123", author_id="user-123", conversation_id="room-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["MODAPI_SECRET_KEY"]) # Submit image for moderation response = client.content.submit( content={ "type": "image", "url": "https://example.com/image.jpg" }, content_id="image-456", author_id="user-123", conversation_id="room-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["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" } } }, content_id="post-789", author_id="user-123", conversation_id="room-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, errors client = ModerationAPI(secret_key=os.environ["MODAPI_SECRET_KEY"]) try: response = client.content.submit( content={ "type": "text", "text": "Hello world!" } ) print(f"Recommendation: {response.recommendation.action}") except errors.RateLimitError as e: print(f"Rate limit exceeded: {e.message}") except errors.ModerationAPIError as e: print(f"API error {e.status_code}: {e.message}") except Exception as e: # network errors, for example a timeout print(f"Request failed: {e}") ``` ## 4. Use async Every method has an async version with the suffix `_async`: ```python theme={"theme":"nord"} import asyncio import os from moderation_api import ModerationAPI async def main(): async with ModerationAPI(secret_key=os.environ["MODAPI_SECRET_KEY"]) as client: response = await client.content.submit_async( content={ "type": "text", "text": "Hello world!" } ) print(response.recommendation.action) asyncio.run(main()) ``` > [!NOTE] > > To retry failed requests, pass `retry_config` when you create the client, or `retries` for one request. You can also set `timeout_ms` on the client or on one 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", "~> 3.0" # Then run bundle install ``` > [!NOTE] > > 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. The SDK returns a response object. The moderation result is in `response.object`. ```ruby Text moderation theme={"theme":"nord"} require "moderation_api" Operations = ModerationApi::Models::Operations # Initialize the client with your secret key client = ModerationApi::ModerationApi.new(secret_key: ENV.fetch("MODAPI_SECRET_KEY")) # Submit text for moderation response = client.content.submit(body: Operations::SubmitContentRequestBody.new( content: Operations::TextRequest.new(text: "Hello world!"), content_id: "message-123", author_id: "user-123", conversation_id: "room-456", metadata: { custom_field: "value" } )) result = response.object # Check if content was flagged if result.evaluation.flagged puts "Content was flagged" end # Act on the recommendation case result.recommendation.action.serialize when "allow" puts "Content is safe." # Save to database or proceed... when "review" puts "Content needs review" # Send to review queue when "reject" puts "Content rejected" # Block the content, show an error, etc... end ``` ```ruby Image moderation theme={"theme":"nord"} require "moderation_api" Operations = ModerationApi::Models::Operations # Initialize the client with your secret key client = ModerationApi::ModerationApi.new(secret_key: ENV.fetch("MODAPI_SECRET_KEY")) # Submit image for moderation response = client.content.submit(body: Operations::SubmitContentRequestBody.new( content: Operations::ImageRequest.new(url: "https://example.com/image.jpg"), content_id: "image-456", author_id: "user-123", conversation_id: "room-456", metadata: { custom_field: "value" } )) result = response.object # Check if content was flagged if result.evaluation.flagged puts "Image was flagged" end # Act on the recommendation case result.recommendation.action.serialize 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" Operations = ModerationApi::Models::Operations # Initialize the client with your secret key client = ModerationApi::ModerationApi.new(secret_key: ENV.fetch("MODAPI_SECRET_KEY")) # Submit object for moderation response = client.content.submit(body: Operations::SubmitContentRequestBody.new( content: Operations::Object.new(data: { name: Operations::DataText.new(text: "John Doe"), email: Operations::DataText.new(text: "john.doe@example.com"), picture: Operations::DataImage.new(url: "https://example.com/image.jpg") }), content_id: "post-789", author_id: "user-123", conversation_id: "room-456", metadata: { custom_field: "value" } )) result = response.object # Check if content was flagged if result.evaluation.flagged puts "Object was flagged" end # Act on the recommendation case result.recommendation.action.serialize 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 raises specific error types for different scenarios: ```ruby theme={"theme":"nord"} require "moderation_api" Operations = ModerationApi::Models::Operations # Initialize the client with your secret key client = ModerationApi::ModerationApi.new(secret_key: ENV.fetch("MODAPI_SECRET_KEY")) Errors = ModerationApi::Models::Errors begin response = client.content.submit(body: Operations::SubmitContentRequestBody.new( content: Operations::TextRequest.new(text: "Hello world!") )) puts "Recommendation: #{response.object.recommendation.action.serialize}" rescue Errors::BadRequestError => e puts "Invalid request: #{e.message}" rescue Errors::AuthenticationError => e puts "Authentication failed: #{e.message}" rescue Errors::RateLimitError => e puts "Rate limit exceeded: #{e.message}" rescue Errors::InternalServerError => e puts "Server error: #{e.message}" rescue Errors::APIError => e puts "API error #{e.status_code}: #{e.message}" end ``` > [!NOTE] > > To set a timeout in milliseconds, pass `timeout_ms` when you create the client, or to one call: `client.content.submit(body: ..., timeout_ms: 5_000)`. *** *** *** # 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 ``` ```bash bun theme={"theme":"nord"} bun add @moderation-api/sdk ``` > [!NOTE] > > Requires Node.js 18 or higher. The SDK also runs on Bun, Deno, Cloudflare Workers and other runtimes with the Fetch API. Use it on your server only, so your API key stays secret. ## 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 > **[Integrations](https://moderationapi.com/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. ![Adding a label filter and saving the result as a new content view](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-build.gif?s=8987e3bbb91c3ecd4a976dfc19ea8c23) Filtering a list, then saving it as a view ## 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). **1. Add the view** Press **+** next to *Content views* in the review sidebar. Give it a name, an icon and an optional description. ![Create new view dialog with name, icon and description](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-create-dialog.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=3f20aedfae39cbb6ba9d891b81f7261d) **2. Filter it** 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**. **3. Save** 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. | > [!NOTE] > > **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. ![Choosing a rule type for a label in the filter menu](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-filter-label-rule.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=d93423239da50bf37405d9184a81f156) | 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). ![A label pill filtering on has matches rather than flagged](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-matched-label.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=2abb94a6ccbc783d3bf55c15b510c04c) ### 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. ![Severity presets and the custom range form](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-severity.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=46f394984701b9a3b86cef4a38773992) ## 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. ![Typing a description and applying it as filters](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-ai-filter.gif?s=5ee914a45a68be77312ccc7ca01e9690) A sentence becomes filter pills you can adjust 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. > [!TIP] > > 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. ![Filter pills with the Save menu open](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-save-menu.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=054e2ee1605073d045807a93d902ae03) Changed filters, and the two ways to keep them * **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. > [!NOTE] > > 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. > [!NOTE] > > 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 ![A content view listing items that need a decision](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-page.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=0ce43396a95bb6ce717f3333ba8d09a0) 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 ![Review sidebar with the inbox pinned above four content views](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-sidebar.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=bf753c3feeb774eb2738a7e209f50019) The inbox is pinned above your saved 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. **1. Content arrives** 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. **2. A moderator decides** 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. **3. The item is resolved** Resolved items leave the pending list. Whether they leave *other* views too is a per-view setting. ## Where to go next > **[Building content views](/review/content-views)** > > Create a view, filter it, and save it for the team. > **[View settings](/review/view-settings)** > > Ordering, resolve behaviour, deduplication and columns. > **[Reviewing content](/review/reviewing-content)** > > Working through a view, item by item. > **[Recipes](/review/recipes/overview)** > > 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**. > [!TIP] > > 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. > [!NOTE] > > 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 **1. Add an escalate action** Create an [action](/actions/content-actions) called **Escalate**. Moderators run it from the item detail, or with Cmd + a number key. **2. Filter on it** In the new view, filter **Action taken → Escalate**. **3. Keep its own list** 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. > [!TIP] > > 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. > [!TIP] > > 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. > [!TIP] > > 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. > [!TIP] > > 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 > **[Escalations](/review/recipes/escalations)** > > A second pair of eyes on the calls no one moderator should make alone. > **[Language views](/review/recipes/language-views)** > > Moderators only see content they can actually read. > **[Channel views](/review/recipes/channel-views)** > > Surfaces with different tolerances get different lists. > **[Content type views](/review/recipes/content-type-views)** > > Listings, profiles and reviews are three jobs, not one. > **[Media review](/review/recipes/media-review)** > > A dedicated shift for images and video, with wellness protections on. ## Getting to the right items first > **[Severity triage](/review/recipes/severity-triage)** > > When the backlog can't be cleared, clear the worst of it. > **[New account review](/review/recipes/new-accounts)** > > A flag on a day-old account is not the same as a flag on a regular. > **[Spam waves](/review/recipes/spam-waves)** > > Two hundred copies of one message, decided once. ## Checking your automation > **[Shadow flag QA](/review/recipes/shadow-flag-qa)** > > See what a rule would catch before it catches anything. > **[Wordlist monitoring](/review/recipes/wordlist-monitoring)** > > Watch terms you've deliberately chosen not to flag. > **[Unflagged sampling](/review/recipes/unflagged-sampling)** > > 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: **1. Priority** Filter **Severity → High (70% and above)**, and set **Ordering → Highest severity**. This is the list that gets staffed first. **2. Everything else** A second view on **Severity → Medium (40–69%)**, worked when there's room. ![Severity band presets in the filter menu](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-severity.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=46f394984701b9a3b86cef4a38773992) ## 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. > [!TIP] > > 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 **1. Put the rule in shadow mode** 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. **2. Build the view** 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. > [!TIP] > > 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 | > [!TIP] > > 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. > [!TIP] > > 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 **1. Set the wordlist to Pass** A [wordlist](/policies/wordlists) in **Pass** mode matches content and records the hits without ever flagging it. **2. Filter on matches, not flags** Filter **Label →** your wordlist **→ Has matches**. ![A label filtered on has matches rather than flagged](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-matched-label.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=2abb94a6ccbc783d3bf55c15b510c04c) ## 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**. > [!TIP] > > 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 ![A content view: chart, tabs, filter bar and list](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-page.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=0ce43396a95bb6ce717f3333ba8d09a0) 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. ![Bulk action bar for selected items](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-bulk-actions.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=4275b629187aace55851a5c357a65ec5) Selected items can be resolved or actioned in bulk Which columns appear is [a view setting](/review/view-settings#columns), including columns built from your own `metadata`. ## An item up close ![Item detail with content, labels, outcome and metadata](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-item-detail.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=edbbc3724bad6e72cb7ecf01e657c6b2) 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. > [!TIP] > > 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. ![Conversation view within an item's detail](https://mintcdn.com/moderationapi/O8C8LNLiFOm8YTLj/images/item-conversation-view.png?fit=max\&auto=format\&n=O8C8LNLiFOm8YTLj\&q=85\&s=e5f1ebcf9ca293f5b0e47734e37ba48a) Surrounding messages from the same conversation ### 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. ![Correcting a decision](https://mintcdn.com/moderationapi/n7BcGOdXBzdto1Xk/images/content-queue-correct.gif?s=716ecd765110f36e9786c2d171a7af7d) Removing a wrong label and adding the right one ### 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. > [!TIP] > > 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. > [!TIP] > > 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). > [!NOTE] > > 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. > [!NOTE] > > 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. ![View settings with ordering, resolve behaviour, deduplication and columns](https://mintcdn.com/moderationapi/MRTaiJ_EUWf575WK/images/view-settings.png?fit=max\&auto=format\&n=MRTaiJ_EUWf575WK\&q=85\&s=5a9f0d2f48781653ca0453d8e2b6a770) ## 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. > [!NOTE] > > 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. > [!TIP] > > 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. # 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 **1. Configure your endpoint** Enable the integration and set your endpoint URL in the dashboard. **2. We request data** When a moderator opens an author, we send a `POST` request to your endpoint with the author's external id and email. **3. You respond with items** Your endpoint returns a list of items describing what to display. **4. We render the sidebar** 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" } ``` **version** `number` The contract version. Currently always `1`. **project\_id** `string` The id of the project the author belongs to. **author\_id** `string` The author's external id — the same `author_id` you send when [submitting content](/api-reference/moderate/analyze-text). **email** `string | null` 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 **text** `object` A label and a value. * `label` (string, optional) * `value` (string, required) * `tooltip` (string, optional) **badge** `object` A colored status pill. * `label` (string, optional) * `value` (string, required) * `variant` (string, optional): one of `default`, `success`, `warning`, `danger`, `info` * `tooltip` (string, optional) **button** `object` 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) **stat** `object` A label with an emphasized value, for numbers and key figures. * `label` (string, required) * `value` (string, required) * `tooltip` (string, optional) **section** `object` A heading that groups the items beneath it. * `label` (string, required) **divider** `object` 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 > [!NOTE] > > 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: ### 1. Signal Collection 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. ### 2. Pattern Recognition 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. ### 3. Risk Scoring 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: > **Behavioral Patterns** > > 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 > **Network & Privacy Indicators** > > 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 > **Account Security Signals** > > 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 ![Author detail page showing trust level, sentiment, flags, and moderation activity](https://mintcdn.com/moderationapi/O8C8LNLiFOm8YTLj/users/author-detail.png?fit=max\&auto=format\&n=O8C8LNLiFOm8YTLj\&q=85\&s=5edd235321ed5797d45d0a0706921373) ## 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 > **[User dashboard](https://dash.moderationapi.com/project/latest/authors)** > > Access the user dashboard to view and manage your users. > **[Author API](/api-reference/author/create-a-new-author)** > > 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). > [!NOTE] > > 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: **1. Submit content with author ID** 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. **2. Sync author details** [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, }, }), }); ``` **3. Monitor trust levels** 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 **4. Take moderation actions** ![User actions dashboard](https://mintcdn.com/moderationapi/97t5arQgQAdQo2TY/users/moderate-action.png?fit=max\&auto=format\&n=97t5arQgQAdQo2TY\&q=85\&s=aefb5c797e5b8ac30164b73a04212ef1) 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. **5. Check user status in your app** 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); ``` **6. Handle webhook notifications** 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); } } ``` > [!NOTE] > > 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. **1. Open the author** Find the user in the [user dashboard](https://dash.moderationapi.com/project/latest/authors) and open their detail page. **2. Choose Update Trust Level** Press the **Moderate** button and choose **Update Trust Level**. **3. Set the level to Trusted** 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. ![Trust level dialog showing an author at Regular level with account age, violation rate, and content counts, a tier bar from Untrusted to Trusted, and Manual Promotion listed as the requirement for Trusted](https://mintcdn.com/moderationapi/bQHS3L9Ro8Ti_z1E/users/trust-level-dialog.png?fit=max\&auto=format\&n=bQHS3L9Ro8Ti_z1E\&q=85\&s=e63da5345e8c8f4c40a2effd962f1815) 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`. > [!WARNING] > > 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). > [!NOTE] > > 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: **name** `string` The author's display name **email** `string` Author's email address **profile\_picture** `string` URL to the author's profile image **external\_link** `string` URL to the author's profile on your platform or other external resources. Will add a clickable link in the dashboard. ### Metadata **metadata** `object` 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: **email\_verified** `boolean` Boolean indicating if email is verified **phone\_verified** `boolean` Boolean indicating if phone number is verified **identity\_verified** `boolean` Boolean indicating if identity has been verified **is\_paying\_customer** `boolean` 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 **manual\_trust\_level** `number` 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: **first\_seen** `number` When the author was first encountered in your system (timestamp in milliseconds) **last\_seen** `number` 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 **API** 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 } ``` **Dashboard** Navigate to the [Users section](https://dash.moderationapi.com/project/latest/authors) in your project dashboard to update user details through the web interface.