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

# Unicode spoofing detection

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

<CodeGroup>
  ```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"
  ```
</CodeGroup>

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

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

## Enable and configure

Unicode spoofing is configured entirely from the dashboard.

<Steps>
  <Step title="Open the policy">
    In your channel, go to **Policies → Spam** and find the **Unicode spoofing**
    card.
  </Step>

  <Step title="Enable detection">
    Toggle **Enable detection** on.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

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

<ResponseField name="flagged" type="boolean">
  Whether an enabled signal fired.
</ResponseField>

<ResponseField name="probability" type="number">
  `1` when an enabled signal fired, `0` otherwise. In shadow flag mode this is
  still `1` while `flagged` stays `false`.
</ResponseField>

<ResponseField name="labels" type="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.
</ResponseField>

<ResponseField name="flagged_fields" type="string[]">
  For object content, the field keys where a signal fired. Omitted for plain
  text.
</ResponseField>

<ResponseField name="data" type="object">
  <Expandable title="properties">
    <ResponseField name="counts.words_total" type="number">
      Total words analyzed.
    </ResponseField>

    <ResponseField name="counts.words_affected" type="number">
      How many words carried a signal.
    </ResponseField>

    <ResponseField name="sample_words" type="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`.
    </ResponseField>
  </Expandable>
</ResponseField>

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

<CodeGroup>
  ```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',
    },
  });
  ```
</CodeGroup>

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

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

### In the response

Normalization surfaces in two fields, whether or not the policy is enabled:

<ResponseField name="evaluation.unicode_spoofed" type="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.
</ResponseField>

<ResponseField name="content.modified" type="string | object">
  The normalized message, present whenever normalization changed the text. If
  masking is also enabled, this field carries the masked *and* normalized
  result.
</ResponseField>

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

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>
