AnvilJSON utilities

Anvil β€Ί Docs β€Ί /v1/text/inspect

Text X-ray API: code points, graphemes, UTF-8/16 and invisible characters

Paste any short string and get an X-ray of what is really inside it: how many grapheme clusters a person sees, how many Unicode code points, how many UTF-8 bytes on the wire, and how long JavaScript's string.length thinks it is (UTF-16 units). Every character is named, categorised and labelled (emoji, zero-width joiner, skin tone, flag letter, control…). Findings call out bidirectional overrides, invisible characters, look-alike letters from other alphabets, mixed scripts and characters that need 4-byte UTF-8 (MySQL utf8mb4).

In one line: GET /v1/text/inspect?text=%F0%9F%91%A8%E2%80%8D%F0%9F%91%A9%E2%80%8D%F0%9F%91%A7&mojibake=1 returns JSON. No key, CORS on, 60 requests a minute.
GET

/v1/text/inspect

Text X-ray: code points, graphemes, UTF-8/16 lengths and invisible characters. Change the values and press Send request to call the live API.

Parameters

NameTypeWhat it doesDefault
text requiredstringThe string to inspect, up to 4 KB of UTF-8. Never stored.β€”
formstringOptional normalisation to return: nfc, nfd, nfkc or nfkdβ€”
mojibakebooleanIf true, also show the UTF-8 bytes misread as Windows-1252false

Example

Request:

GET https://api.oddlathe.com/v1/text/inspect?text=%F0%9F%91%A8%E2%80%8D%F0%9F%91%A9%E2%80%8D%F0%9F%91%A7&mojibake=1

Response (generated by the real code when these docs were last built):

{
 "text": "πŸ‘¨β€πŸ‘©β€πŸ‘§",
 "grade": "clean",
 "summary": {
  "graphemes": 1,
  "codepoints": 5,
  "utf8_bytes": 18,
  "utf16_units": 8,
  "js_length": 8,
  "is_nfc": true,
  "is_nfd": true,
  "is_nfkc": true,
  "scripts": [
   "Emoji"
  ]
 },
 "utf8_hex": "F0 9F 91 A8 E2 80 8D F0 9F 91 A9 E2 80 8D F0 9F 91 A7",
 "utf16_hex": "D83D DC68 200D D83D DC69 200D D83D DC67",
 "normalization": {},
 "findings": [
  {
   "code": "utf16_vs_codepoints",
   "severity": "info",
   "message": "JavaScript/Java string length is 8 (UTF-16 units) but there are 5 Unicode code points (astral characters need a surrogate pair)."
  },
  {
   "code": "needs_4byte",
   "severity": "info",
   "message": "Contains characters above U+FFFF (4-byte UTF-8). MySQL's old utf8/utf8mb3 charset would reject or squash them β€” use utf8mb4."
  }
 ],
 "goods": [
  "Contains zero-width joiners β€” likely a multi-person or multi-part emoji.",
  "UTF-8 uses 18 bytes for 5 code point(s).",
  "1 grapheme cluster(s) from 5 code point(s) β€” some characters combine visually."
 ],
 …

Code examples

curl
curl -G 'https://api.oddlathe.com/v1/text/inspect' \
  --data-urlencode 'text=πŸ‘¨β€πŸ‘©β€πŸ‘§' \
  --data-urlencode 'mojibake=1'
JavaScript (browser or Node 18+)
const params = new URLSearchParams({
  text: "πŸ‘¨β€πŸ‘©β€πŸ‘§",
  mojibake: "1"
});
const res = await fetch(`https://api.oddlathe.com/v1/text/inspect?${params}`);
if (!res.ok) throw new Error((await res.json()).error.message);
const data = await res.json();
console.log(data);
Python (standard library only)
import json, urllib.parse, urllib.request

params = urllib.parse.urlencode({"text": "πŸ‘¨β€πŸ‘©β€πŸ‘§", "mojibake": "1"})
url = "https://api.oddlathe.com/v1/text/inspect?" + params
# Send a User-Agent: the edge network refuses Python's default one.
req = urllib.request.Request(url, headers={"User-Agent": "my-script/1.0"})
with urllib.request.urlopen(req, timeout=10) as r:
    data = json.load(r)
print(data)

When it's useful

  • Explaining why an emoji 'is 8 characters long' in JavaScript but one picture on screen
  • Catching bidi / zero-width spoofing in usernames, hostnames or chat before you store them
  • Debugging mojibake (UTF-8 read as Windows-1252) and MySQL 'Incorrect string value' / utf8mb3 issues
  • CI checks that display names don't contain RLO, soft hyphens or homoglyphs
  • Teaching Unicode: graphemes vs code points vs bytes

Good to know

  • Pass text (required, up to 4 KB of UTF-8). Optional form=nfc|nfd|nfkc|nfkd returns that normalised form under normalized. Optional mojibake=1 adds a Windows-1252 misread preview of the UTF-8 bytes.
  • grade is clean, notable (warnings) or suspicious (bidi overrides / lone surrogates). Each item in findings has a code, a severity (error, warn, info) and a plain-words message.
  • Finding codes include bidi_override, invisible, bom, homoglyph, mixed_scripts, utf16_vs_codepoints, not_nfc, nfkc_changes, needs_4byte, tag_chars, noncharacter and surrogate.
  • summary.js_length matches JavaScript / Java / .NET string length (UTF-16 code units). A family emoji is often 1 grapheme, 5 code points, 8 UTF-16 units and 18 UTF-8 bytes.
  • Grapheme clustering follows common emoji rules (ZWJ, variation selectors, skin tones, keycaps, tags, regional-indicator pairs) plus combining marks β€” enough for emoji X-rays without a full UAX #29 dependency.
  • Detail lists are capped (80 graphemes / 200 code points); truncated says when that happened. Nothing is fetched or stored.
  • For a clickable kid-friendly X-ray, see Wirebook's emoji lesson. For tofu boxes, mojibake and utf8mb4 fixes see Tooltern's emoji tofu, mojibake and utf8mb4 guides.

X-rays a short string the way Unicode sees it: grapheme clusters vs code points vs UTF-8 bytes vs JavaScript string.length (UTF-16 units), every character named and labelled (emoji, zero-width joiner, skin tone, flag letter, control…), and findings for bidirectional overrides, invisible characters, look-alike letters from other alphabets, mixed scripts and characters that need 4-byte UTF-8. Optional form= returns a normalised copy; mojibake=1 previews the classic Windows-1252 misread. Pure text analysis: nothing is fetched or stored.

Errors

Errors are JSON like {"error": {"code": "…", "message": "…"}}. The ones this endpoint can return:

  • missing_param (400): a required parameter is missing or empty
  • bad_param (400): a value is the wrong type or out of range (the message says which)
  • too_long (413): text is over 4 KB
  • rate_limited (429): over 60 requests a minute; wait for Retry-After seconds

Questions

Why is πŸ˜€ length 2 in JavaScript?

JavaScript counts UTF-16 code units. Characters above U+FFFF need a surrogate pair, so one emoji is length 2. This API reports both <code>codepoints</code> and <code>js_length</code>.

What's a grapheme?

What a person sees as one character. A family emoji is several code points glued with zero-width joiners, but one grapheme.

Can it tell me a string is safe?

No. It flags common spoofing ingredients (bidi overrides, invisible chars, look-alikes). A clean grade is not a security verdict.

Does it fetch anything?

Never. It only looks at the characters you sent.

Related endpoints

Prefer a page to an API?

Published Β· OpenAPI 3.1 spec Β· Limits and privacy