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

# Understand and act on API 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.
</Note>

***

## 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.
</Note>

### 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`.
</Note>

***

## 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.
