---
title: "Unicode normalization (NFC/NFKC), script detection and homoglyph/confusable check"
method: GET
path: "/v1/text/unicode"
tags: ["deterministic", "no-api-key", "text", "unicode", "security"]
---

# Unicode normalization (NFC/NFKC), script detection and homoglyph/confusable check

`GET /v1/text/unicode`

## Request

- Base URL: `https://api.duoleads.com`
- URL: `https://api.duoleads.com/v1/text/unicode`
- Auth: none declared

Unicode normalization and confusables check: apply NFC, NFD, NFKC or NFKD, report which forms a string is already in, list the scripts present (all Unicode scripts), and flag confusable and homoglyph characters — Cyrillic а in a Latin word, fullwidth or mathematical look-alikes, all-look-alike words — plus zero-width joiners, bidirectional overrides, invisible and blank characters. Returns per-character detail with code points, names and the ASCII character each imitates.

**Output (`data`):** normalised, form, already_normalised {nfc, nfd, nfkc, nfkd}, lengths {code_points, graphemes, utf8_bytes, utf16_units} (of the normalised output), scripts (every Unicode Script property value present, from Unicode 18 Scripts.txt; Common and Inherited are not listed), mixed_script, suspicious [{index (code point index in the input), char, code_point, name (Unicode character name; string|null, null only when the name is not vendored: only ASCII, Latin-1, spaces, combining marks, format and default-ignorable characters, controls and confusable look-alikes have names), category, kind confusable|invisible|bidi|combining_excess, looks_like (string|null, null unless kind is confusable)}], ascii_safe (input is printable ASCII only); with detail=full also characters [{index, char, code_point, name (string|null, as above), category, script (string|null, null for Common/Inherited/unassigned)}] (first 256) and characters_truncated. Confusables (UTS #39 confusables.txt, the entries that map to one ASCII character): in mixed-script text every one is reported; in single-script text the compatibility look-alikes of ASCII letters and digits (fullwidth, mathematical, Roman numerals) and words written wholly in one non-Latin script whose every letter has an ASCII look-alike (whole-script confusables, e.g. Cyrillic рау). Invisible = format characters (Cf), Default_Ignorable_Code_Point (e.g. Hangul filler U+3164, variation selectors) and U+2800 braille blank; bidi = embeddings, overrides, isolates and directional marks; combining_excess = the 4th and later combining marks in a row.

Deterministic: identical input returns identical output, so the response is served with `Cache-Control: private, max-age=31536000, immutable` (the paying client may cache it indefinitely; shared caches must not, because the response is paid).

Sources: unicode-confusables 18.0.0 (published 2026-08-06, Unicode License v3). Echoed in `meta.sources`.

Price: $0.05 USDC per call (x402, Base). Route contract version 1.0.0. Docs: https://api.duoleads.com/docs#text-unicode

## Query parameters

- `text` string, required
- `form` 'nfc' | 'nfd' | 'nfkc' | 'nfkd'
- `detail` 'summary' | 'full'

## Response `200`

Paid and settled. `data` is route-specific; a route that judges its input returns 200 with `data.valid: false` for a bad value.

- object
  - `ok` unknown, required
  - `route` '/v1/text/unicode', required
  - `version` '1.0.0', required
  - `data` object, required: normalised, form, already_normalised {nfc, nfd, nfkc, nfkd}, lengths {code_points, graphemes, utf8_bytes, utf16_units} (of the normalised output), scripts (every Unicode Script property value present, from Unicode 18 Scripts.txt; Common and Inherited are not listed), mixed_script, suspicious [{index (code point index in the input), char, code_point, name (Unicode character name; string|null, null only when the name is not vendored: only ASCII, Latin-1, spaces, combining marks, format and default-ignorable characters, controls and confusable look-alikes have names), category, kind confusable|invisible|bidi|combining_excess, looks_like (string|null, null unless kind is confusable)}], ascii_safe (input is printable ASCII only); with detail=full also characters [{index, char, code_point, name (string|null, as above), category, script (string|null, null for Common/Inherited/unassigned)}] (first 256) and characters_truncated. Confusables (UTS #39 confusables.txt, the entries that map to one ASCII character): in mixed-script text every one is reported; in single-script text the compatibility look-alikes of ASCII letters and digits (fullwidth, mathematical, Roman numerals) and words written wholly in one non-Latin script whose every letter has an ASCII look-alike (whole-script confusables, e.g. Cyrillic рау). Invisible = format characters (Cf), Default_Ignorable_Code_Point (e.g. Hangul filler U+3164, variation selectors) and U+2800 braille blank; bidi = embeddings, overrides, isolates and directional marks; combining_excess = the 4th and later combining marks in a row.
    - `normalised` string, required
    - `form` string, required
    - `already_normalised` object, required
      - `nfc` boolean, required
      - `nfd` boolean, required
      - `nfkc` boolean, required
      - `nfkd` boolean, required
    - `lengths` object, required
      - `code_points` integer, required
      - `graphemes` integer, required
      - `utf8_bytes` integer, required
      - `utf16_units` integer, required
    - `scripts` string[], required
    - `mixed_script` boolean, required
    - `suspicious` object[], required
      - `index` integer, required
      - `char` string, required
      - `code_point` string, required
      - `name` string, nullable, required
      - `category` string, required
      - `kind` string, required
      - `looks_like` string, nullable, required
    - `ascii_safe` boolean, required
    - `characters` object[]
      - `index` integer, required
      - `char` string, required
      - `code_point` string, required
      - `name` string, nullable, required
      - `category` string, required
      - `script` string, nullable, required
    - `characters_truncated` boolean
  - `meta` Meta, required
    - `disclaimer` string, required: Legal short disclaimer, identical on every response.
    - `request_id` string, required: ULID.
    - `computed_at` string, date-time, required: RFC 3339 UTC with milliseconds.
    - `price_usd` string, required: Decimal string, "0.05".
    - `deterministic` boolean, required
    - `sources` object[]: Present only on routes that read a vendored dataset: the exact edition that answered.
      - `id` string, required
      - `version` string, required
      - `published` string, required
      - `attribution` string
      - `licence` string

## Other responses

- `400`: MISSING_PARAM, INVALID_PARAM or UNSUPPORTED_VALUE. Not charged: validation runs before the paywall.
- `402`: Payment required. No valid PAYMENT-SIGNATURE was supplied: read the base64 PAYMENT-REQUIRED header (or this body) for the price ($0.05 = 50000 atomic USDC on eip155:8453), sign an EIP-3009 authorization and retry. Also returned when settlement is declined (PAYMENT_FAILED). Nothing is charged on a 402.
- `404`: NOT_FOUND: unknown path. Not charged.
- `405`: METHOD_NOT_ALLOWED: only GET and HEAD are served. Not charged.
- `413`: INPUT_TOO_LARGE: a parameter exceeded its documented maximum length. Not charged.
- `500`: INTERNAL: a server bug. Not charged.

---

[API](https://skmtc.dev/duoleads/apis/duo-data-utilities-iban-isin-vat-tax-id-barcode-timezone-geo.md) · [All operations](https://skmtc.dev/duoleads/apis/duo-data-utilities-iban-isin-vat-tax-id-barcode-timezone-geo/llms.txt) · [OpenAPI document](https://skmtc.dev/duoleads/apis/duo-data-utilities-iban-isin-vat-tax-id-barcode-timezone-geo/revisions/18790d59615e?raw)
