Skip to main content

Mojibake Fixer — Repair é, ’ and привет in 28 Encodings

REAL DATA

Diagnose and repair mojibake — the byte round-trip that turned café into café and привет into привет — across 28 legacy encodings.

Authoritative reference data or standards computation

Base URL
/api/text-encoding
Capabilities
2 routes
Last updated
July 29, 2026
Data sources · 2

GET /api/text-encoding/diagnose

Encodes the input back through all 28 supported legacy encoders and reads the bytes as strict UTF-8. Returns the byte evidence, a labelled-confidence hypothesis, every rival reading and the recovered text — or an honest none when nothing decodes cleanly.

Live requestRuns against the public API
No key required
GET/api/text-encoding/diagnose?text=caf%C3%83%C2%A9

Request parameters

The suspect text, exactly as it appears (whitespace is preserved). Maximum 2000 UTF-16 code units.

How many stacked broken decodes to unwind. 1 diagnoses single mojibake only; 2 also catches double-encoded text such as café. When this ceiling stops the unwind early the answer is marked `complete: false` and warns, never silently returned as finished.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/text-encoding/diagnose?text=caf%C3%83%C2%A9"
const res = await fetch("https://randomapi.dev/api/text-encoding/diagnose?text=caf%C3%83%C2%A9");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/text-encoding/diagnose?text=caf%C3%83%C2%A9").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/text-encoding/diagnose?text=caf%C3%83%C2%A9"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 2
text string required

The suspect text, exactly as it appears (whitespace is preserved). Maximum 2000 UTF-16 code units.

allowed: ≥ 1
example: text=café
layers int

How many stacked broken decodes to unwind. 1 diagnoses single mojibake only; 2 also catches double-encoded text such as café. When this ceiling stops the unwind early the answer is marked `complete: false` and warns, never silently returned as finished.

default: 2
allowed: 1 – 3
example: layers=3
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=input,hypothesis
exclude list

Return all fields except these (comma-separated).

example: exclude=complete
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 10
input string

The suspect text exactly as received; whitespace is never trimmed.

example: café

hypothesis string

`utf8-as-<encoding>` with the punctuation removed (utf8-as-windows1252, utf8-as-windows1251, utf8-as-koi8r…), plus the two historical spellings utf8-as-latin1 (ISO-8859-1) and utf8-as-latin9 (ISO-8859-15); `double-encoded` for two or more layers; `none` when nothing decodes cleanly.

example: utf8-as-windows1252

encoding string nullable

The encoding this hypothesis blames — the one whose decoder would have produced the input. Null when there is no recovery. Use this instead of parsing `hypothesis`.

example: windows-1252

confidence string

high | medium | low — how strong the byte evidence is for this hypothesis.

example: medium

confidenceRationale string

Plain-English reason for the confidence label, naming the actual bytes, how many rival encodings agree or disagree and any reason the answer is not final.

example: Encoding the input as windows-1252 gives 5 byte(s) holding a single 2-byte UTF-8 sequence. It decodes cleanly, but a string can legitimately contain those two characters, so this stays a hypothesis. iso-8859-1, iso-8859-15, windows-1254 and 1 more produce the identical recovery; macintosh also decodes, but to a different string.

evidence object[]

The byte trail: one row per non-ASCII character with its code point, the byte it encodes to, its zero-based code-point position and its role in the UTF-8 sequence. Capped at 32 rows.

example: [{"codePoint":"U+00C3","byte":"0xC3","position":3,"note":"Byte 1 of a 2-byte UTF-8 sequence decoding to U+00E9 'é'."},{"codePoint":"U+00A9","byte":"0xA9","position":4,"note":"Byte 2 of 2 in the UTF-8 sequence for U+00E9 'é'."}]

recovered string nullable

The text the hypothesis recovers, or null when no clean recovery exists.

example: café

recoveredLayers integer

How many broken decodes were undone; 0 when nothing was recovered.

example: 1

alternatives object[]

Every other supported encoding that also unwinds the input, grouped by the text it recovers (`recovered`, `layers`, `encodings`). A group matching `recovered` above corroborates the answer; one that differs is genuine ambiguity and is listed first. Capped at 8 groups.

example: [{"recovered":"caf̩","layers":1,"encodings":["macintosh"]},{"recovered":"café","layers":1,"encodings":["iso-8859-1","iso-8859-15","windows-1254","iso-8859-14"]}]

complete boolean nullable

True when the recovery no longer unwinds under any supported encoding. False when it still does — normally because `layers` ran out, so the text is only partly repaired. Null when there is no recovery.

example: true

Documented examples

Build-generated requests and complete responses
7
Recover café from café
GET /api/text-encoding/diagnose?text=caf%C3%83%C2%A9
{
  "data": {
    "input": "café",
    "hypothesis": "utf8-as-windows1252",
    "encoding": "windows-1252",
    "confidence": "medium",
    "confidenceRationale": "Encoding the input as windows-1252 gives 5 byte(s) holding a single 2-byte UTF-8 sequence. It decodes cleanly, but a string can legitimately contain those two characters, so this stays a hypothesis. iso-8859-1, iso-8859-15, windows-1254 and 1 more produce the identical recovery; macintosh also decodes, but to a different string.",
    "evidence": [
      {
        "codePoint": "U+00C3",
        "byte": "0xC3",
        "position": 3,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+00E9 'é'."
      },
      {
        "codePoint": "U+00A9",
        "byte": "0xA9",
        "position": 4,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+00E9 'é'."
      }
    ],
    "recovered": "café",
    "recoveredLayers": 1,
    "alternatives": [
      {
        "recovered": "caf̩",
        "layers": 1,
        "encodings": [
          "macintosh"
        ]
      },
      {
        "recovered": "café",
        "layers": 1,
        "encodings": [
          "iso-8859-1",
          "iso-8859-15",
          "windows-1254",
          "iso-8859-14"
        ]
    …
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
Recover Russian привет from привет
GET /api/text-encoding/diagnose?text=%C3%90%C2%BF%C3%91%E2%82%AC%C3%90%C2%B8%C3%90%C2%B2%C3%90%C2%B5%C3%91%E2%80%9A
{
  "data": {
    "input": "привет",
    "hypothesis": "utf8-as-windows1252",
    "encoding": "windows-1252",
    "confidence": "high",
    "confidenceRationale": "Encoding the input as windows-1252 gives 12 byte(s) holding 6 well-formed multi-byte UTF-8 sequence(s), the longest 2 bytes — a trail that is very hard to write by accident.",
    "evidence": [
      {
        "codePoint": "U+00D0",
        "byte": "0xD0",
        "position": 0,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+043F 'п'."
      },
      {
        "codePoint": "U+00BF",
        "byte": "0xBF",
        "position": 1,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+043F 'п'."
      },
      {
        "codePoint": "U+00D1",
        "byte": "0xD1",
        "position": 2,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+0440 'р'."
      },
      {
        "codePoint": "U+20AC",
        "byte": "0x80",
        "position": 3,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+0440 'р'."
      },
      {
        "codePoint": "U+00D0",
        "byte": "0xD0",
        "position": 4,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+0438 'и'."
      },
      {
        "codePoint": "U+00B8",
    …
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
Unwind double-encoded café
GET /api/text-encoding/diagnose?text=caf%C3%83%C6%92%C3%82%C2%A9
{
  "data": {
    "input": "café",
    "hypothesis": "double-encoded",
    "encoding": "windows-1252",
    "confidence": "high",
    "confidenceRationale": "Encoding the input as windows-1252 gives 7 byte(s) holding 2 well-formed multi-byte UTF-8 sequence(s), the longest 2 bytes, and the result needed 2 decode layers to come back — a trail that is very hard to write by accident. windows-1254 produces the identical recovery.",
    "evidence": [
      {
        "codePoint": "U+00C3",
        "byte": "0xC3",
        "position": 3,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+00C3 'Ã'."
      },
      {
        "codePoint": "U+0192",
        "byte": "0x83",
        "position": 4,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+00C3 'Ã'."
      },
      {
        "codePoint": "U+00C2",
        "byte": "0xC2",
        "position": 5,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+00A9 '©'."
      },
      {
        "codePoint": "U+00A9",
        "byte": "0xA9",
        "position": 6,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+00A9 '©'."
      }
    ],
    "recovered": "café",
    "recoveredLayers": 2,
    "alternatives": [
      {
        "recovered": "café",
        "layers": 2,
        "encodings": [
    …
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
Stop at one layer and be told it is unfinished
GET /api/text-encoding/diagnose?text=caf%C3%83%C6%92%C3%82%C2%A9&layers=1
{
  "data": {
    "input": "café",
    "hypothesis": "utf8-as-windows1252",
    "encoding": "windows-1252",
    "confidence": "medium",
    "confidenceRationale": "Encoding the input as windows-1252 gives 7 byte(s) holding 2 well-formed multi-byte UTF-8 sequence(s), the longest 2 bytes — a trail that is very hard to write by accident. windows-1254 produces the identical recovery. All 1 layer(s) allowed by 'layers' were used and the recovery still unwinds, so this is a partial repair.",
    "evidence": [
      {
        "codePoint": "U+00C3",
        "byte": "0xC3",
        "position": 3,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+00C3 'Ã'."
      },
      {
        "codePoint": "U+0192",
        "byte": "0x83",
        "position": 4,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+00C3 'Ã'."
      },
      {
        "codePoint": "U+00C2",
        "byte": "0xC2",
        "position": 5,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+00A9 '©'."
      },
      {
        "codePoint": "U+00A9",
        "byte": "0xA9",
        "position": 6,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+00A9 '©'."
      }
    ],
    "recovered": "café",
    "recoveredLayers": 1,
    "alternatives": [
      {
        "recovered": "café",
        "layers": 1,
        "encodings": [
    …
    ]
  }
}
Explain a smart quote turned into ’
GET /api/text-encoding/diagnose?text=Don%C3%A2%E2%82%AC%E2%84%A2t%20panic
{
  "data": {
    "input": "Don’t panic",
    "hypothesis": "utf8-as-windows1252",
    "encoding": "windows-1252",
    "confidence": "high",
    "confidenceRationale": "Encoding the input as windows-1252 gives 13 byte(s) holding 1 well-formed multi-byte UTF-8 sequence(s), the longest 3 bytes — a trail that is very hard to write by accident. windows-1250, windows-1256, windows-1254 and 1 more produce the identical recovery.",
    "evidence": [
      {
        "codePoint": "U+00E2",
        "byte": "0xE2",
        "position": 3,
        "note": "Byte 1 of a 3-byte UTF-8 sequence decoding to U+2019 '’'."
      },
      {
        "codePoint": "U+20AC",
        "byte": "0x80",
        "position": 4,
        "note": "Byte 2 of 3 in the UTF-8 sequence for U+2019 '’'."
      },
      {
        "codePoint": "U+2122",
        "byte": "0x99",
        "position": 5,
        "note": "Byte 3 of 3 in the UTF-8 sequence for U+2019 '’'."
      }
    ],
    "recovered": "Don’t panic",
    "recoveredLayers": 1,
    "alternatives": [
      {
        "recovered": "Don’t panic",
        "layers": 1,
        "encodings": [
          "windows-1250",
          "windows-1256",
          "windows-1254",
          "windows-1258"
        ]
      }
    …
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
ISO-8859-15 is the only reading that gives Ærø
GET /api/text-encoding/diagnose?text=%C3%83%C2%86r%C3%83%C5%BE
{
  "data": {
    "input": "ÆrÞ",
    "hypothesis": "utf8-as-latin9",
    "encoding": "iso-8859-15",
    "confidence": "high",
    "confidenceRationale": "Encoding the input as iso-8859-15 gives 5 byte(s) holding 2 well-formed multi-byte UTF-8 sequence(s), the longest 2 bytes — a trail that is very hard to write by accident. iso-8859-4 and iso-8859-10 also decode, but to different strings.",
    "evidence": [
      {
        "codePoint": "U+00C3",
        "byte": "0xC3",
        "position": 0,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+00C6 'Æ'."
      },
      {
        "codePoint": "U+0086",
        "byte": "0x86",
        "position": 1,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+00C6 'Æ'."
      },
      {
        "codePoint": "U+00C3",
        "byte": "0xC3",
        "position": 3,
        "note": "Byte 1 of a 2-byte UTF-8 sequence decoding to U+00F8 'ø'."
      },
      {
        "codePoint": "U+017E",
        "byte": "0xB8",
        "position": 4,
        "note": "Byte 2 of 2 in the UTF-8 sequence for U+00F8 'ø'."
      }
    ],
    "recovered": "Ærø",
    "recoveredLayers": 1,
    "alternatives": [
      {
        "recovered": "Ærþ",
        "layers": 1,
        "encodings": [
    …
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
Clean text is reported as no mojibake
GET /api/text-encoding/diagnose?text=caf%C3%A9
{
  "data": {
    "input": "café",
    "hypothesis": "none",
    "encoding": null,
    "confidence": "high",
    "confidenceRationale": "No supported single-byte encoding turns the input into well-formed UTF-8 bytes, and no UTF-8 lead byte is followed by a continuation byte, so there is no mojibake trail to explain.",
    "evidence": [
      {
        "codePoint": "U+00E9",
        "byte": "0xE9",
        "position": 3,
        "note": "Not part of a well-formed UTF-8 sequence."
      }
    ],
    "recovered": null,
    "recoveredLayers": 0,
    "alternatives": [],
    "complete": null
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/diagnose",
    "params": {
      "text": "café",
      "layers": 2
    },
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}

GET /api/text-encoding/transcode

Encodes the text with from, then decodes those exact bytes with to. Mismatch the pair and you get reproducible mojibake for your fixtures; match it and you get a plain byte dump.

Live requestRuns against the public API
No key required
GET/api/text-encoding/transcode?text=caf%C3%A9&to=windows-1252

Request parameters

The text to round-trip, exactly as given (whitespace is preserved). Maximum 2000 UTF-16 code units.

Encoding used to turn the text into bytes. Characters it cannot represent are reported in `unmappable`, never replaced.

Encoding used to read those bytes back. 20 of the 28 single-byte targets decode all 256 byte values; the other 8 leave some bytes unassigned, which decode to U+FFFD with a warning. `utf-8` uses the Encoding Standard's U+FFFD replacement mode.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/text-encoding/transcode?text=caf%C3%A9&to=windows-1252"
const res = await fetch("https://randomapi.dev/api/text-encoding/transcode?text=caf%C3%A9&to=windows-1252");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/text-encoding/transcode?text=caf%C3%A9&to=windows-1252").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/text-encoding/transcode?text=caf%C3%A9&to=windows-1252"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 3
text string required

The text to round-trip, exactly as given (whitespace is preserved). Maximum 2000 UTF-16 code units.

allowed: ≥ 1
example: text=café
from enum

Encoding used to turn the text into bytes. Characters it cannot represent are reported in `unmappable`, never replaced.

default: utf-8
allowed: utf-8 | windows-1252 | iso-8859-1 | iso-8859-15 | windows-1251 | windows-1250 | windows-1256 | windows-1254 | windows-1253 | windows-1255 | windows-1257 | windows-1258 | windows-874 | koi8-r | koi8-u | ibm866 | iso-8859-2 | iso-8859-5 | iso-8859-7 | iso-8859-6 | iso-8859-8 | iso-8859-4 | iso-8859-13 | iso-8859-3 | iso-8859-10 | iso-8859-14 | iso-8859-16 | macintosh | x-mac-cyrillic
example: from=windows-1252
to enum required

Encoding used to read those bytes back. 20 of the 28 single-byte targets decode all 256 byte values; the other 8 leave some bytes unassigned, which decode to U+FFFD with a warning. `utf-8` uses the Encoding Standard's U+FFFD replacement mode.

allowed: utf-8 | windows-1252 | iso-8859-1 | iso-8859-15 | windows-1251 | windows-1250 | windows-1256 | windows-1254 | windows-1253 | windows-1255 | windows-1257 | windows-1258 | windows-874 | koi8-r | koi8-u | ibm866 | iso-8859-2 | iso-8859-5 | iso-8859-7 | iso-8859-6 | iso-8859-8 | iso-8859-4 | iso-8859-13 | iso-8859-3 | iso-8859-10 | iso-8859-14 | iso-8859-16 | macintosh | x-mac-cyrillic
example: to=windows-1252
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=input,from
exclude list

Return all fields except these (comma-separated).

example: exclude=unmappable
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 7
input string

The text exactly as received; whitespace is never trimmed.

example: café

from string

Encoding used to turn the text into bytes.

example: utf-8

to string

Encoding used to read those bytes back.

example: windows-1252

bytes string (space-separated uppercase hex)

The exact intermediate bytes, one two-digit pair per byte.

example: 63 61 66 C3 A9

byteLength integer

Number of intermediate bytes.

example: 5

output string

Result of decoding those bytes with the target encoding — the mojibake, when the pair is mismatched.

example: café

unmappable object[]

Characters the source encoding cannot represent, with their zero-based code-point position. They are omitted from the bytes, never substituted; always empty for from=utf-8, which can represent every Unicode scalar value.

example: [{"codePoint":"U+20AC","position":0}]

Documented examples

Build-generated requests and complete responses
8
Turn café into café
GET /api/text-encoding/transcode?text=caf%C3%A9&to=windows-1252
{
  "data": {
    "input": "café",
    "from": "utf-8",
    "to": "windows-1252",
    "bytes": "63 61 66 C3 A9",
    "byteLength": 5,
    "output": "café",
    "unmappable": []
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/transcode",
    "params": {
      "text": "café",
      "from": "utf-8",
      "to": "windows-1252"
    },
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
Garble Russian привет through windows-1252
GET /api/text-encoding/transcode?text=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82&to=windows-1252
{
  "data": {
    "input": "привет",
    "from": "utf-8",
    "to": "windows-1252",
    "bytes": "D0 BF D1 80 D0 B8 D0 B2 D0 B5 D1 82",
    "byteLength": 12,
    "output": "привет",
    "unmappable": []
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/transcode",
    "params": {
      "text": "привет",
      "from": "utf-8",
      "to": "windows-1252"
    },
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
Emoji mojibake needs true latin1
GET /api/text-encoding/transcode?text=%F0%9F%98%80&to=iso-8859-1
{
  "data": {
    "input": "😀",
    "from": "utf-8",
    "to": "iso-8859-1",
    "bytes": "F0 9F 98 80",
    "byteLength": 4,
    "output": "😀",
    "unmappable": []
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/transcode",
    "params": {
      "text": "😀",
      "from": "utf-8",
      "to": "iso-8859-1"
    },
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
Windows bytes reread as Mac Roman
GET /api/text-encoding/transcode?text=Gr%C3%BC%C3%9Fe&from=windows-1252&to=macintosh
{
  "data": {
    "input": "Grüße",
    "from": "windows-1252",
    "to": "macintosh",
    "bytes": "47 72 FC DF 65",
    "byteLength": 5,
    "output": "Gr¸fle",
    "unmappable": []
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/transcode",
    "params": {
      "text": "Grüße",
      "from": "windows-1252",
      "to": "macintosh"
    },
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
Greek bytes reread as KOI8-R
GET /api/text-encoding/transcode?text=%CE%95%CE%BB%CE%BB%CE%AC%CE%B4%CE%B1&from=windows-1253&to=koi8-r
{
  "data": {
    "input": "Ελλάδα",
    "from": "windows-1253",
    "to": "koi8-r",
    "bytes": "C5 EB EB DC E4 E1",
    "byteLength": 6,
    "output": "еККэДА",
    "unmappable": []
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/transcode",
    "params": {
      "text": "Ελλάδα",
      "from": "windows-1253",
      "to": "koi8-r"
    },
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
The euro sign has no ISO-8859-1 byte
GET /api/text-encoding/transcode?text=%E2%82%AC10&from=iso-8859-1&to=utf-8
{
  "data": {
    "input": "€10",
    "from": "iso-8859-1",
    "to": "utf-8",
    "bytes": "31 30",
    "byteLength": 2,
    "output": "10",
    "unmappable": [
      {
        "codePoint": "U+20AC",
        "position": 0
      }
    ]
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/transcode",
    "params": {
      "text": "€10",
      "from": "iso-8859-1",
      "to": "utf-8"
    },
    "generatedAt": "2026-07-29T08:14:28.000Z",
    "warnings": [
      "1 character(s) have no iso-8859-1 byte and were omitted from 'bytes' rather than substituted; they are listed in 'unmappable'."
    ]
  }
}
ISO-8859-15 puts the euro at 0xA4
GET /api/text-encoding/transcode?text=%E2%82%AC10&from=iso-8859-15&to=windows-1252
{
  "data": {
    "input": "€10",
    "from": "iso-8859-15",
    "to": "windows-1252",
    "bytes": "A4 31 30",
    "byteLength": 3,
    "output": "¤10",
    "unmappable": []
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/transcode",
    "params": {
      "text": "€10",
      "from": "iso-8859-15",
      "to": "windows-1252"
    },
    "generatedAt": "2026-07-29T08:14:28.000Z"
  }
}
ISO-8859-6 leaves 45 bytes unassigned
GET /api/text-encoding/transcode?text=%C2%A1%C2%A2%C2%A3&from=iso-8859-1&to=iso-8859-6
{
  "data": {
    "input": "¡¢£",
    "from": "iso-8859-1",
    "to": "iso-8859-6",
    "bytes": "A1 A2 A3",
    "byteLength": 3,
    "output": "���",
    "unmappable": []
  },
  "meta": {
    "endpoint": "text-encoding",
    "route": "/transcode",
    "params": {
      "text": "¡¢£",
      "from": "iso-8859-1",
      "to": "iso-8859-6"
    },
    "generatedAt": "2026-07-29T08:14:28.000Z",
    "warnings": [
      "3 byte(s) (0xA1, 0xA2 and 0xA3) are unassigned in iso-8859-6 and decoded to the U+FFFD replacement character, as the Encoding Standard's single-byte decoder specifies; iso-8859-6 leaves 45 of its 128 high bytes unassigned."
    ]
  }
}

About this API

Coverage & behavior

Mojibake is what you get when text is encoded as UTF-8 and then read back with a single-byte legacy encoding: café becomes café, don’t becomes don’t, привет becomes привет, Ελλάδα becomes Ελλάδα. This endpoint does both directions of that round-trip using the WHATWG Encoding Standard's index tables — all 27 of them, plus the ISO/IEC 8859-1 identity mapping, which the Encoding Standard does not publish because it hands that label to windows-1252. The tables are bundled and pinned (each with its own publication date and the SHA-256 of the exact file) rather than delegated to TextDecoder — legacy encoding labels are not guaranteed to exist in every runtime, and an answer that changes with the host is not an answer.

The 28 supported encodings

Every name below is a registered Encoding Standard label — with one deliberate exception, iso-8859-1, which the Standard maps onto windows-1252 and this API keeps separate (see below). The order here is the order /diagnose breaks ties in — roughly how often each one is the culprit in a real mojibake report.

  • Western Europeanwindows-1252, iso-8859-1, iso-8859-15, iso-8859-16, macintosh (Mac OS Roman)
  • Central & Eastern European, Balticwindows-1250, windows-1257, iso-8859-2, iso-8859-4, iso-8859-13, iso-8859-10 (Nordic), iso-8859-3 (South European), iso-8859-14 (Celtic)
  • Cyrillicwindows-1251, koi8-r, koi8-u, iso-8859-5, ibm866, x-mac-cyrillic
  • Greekwindows-1253, iso-8859-7
  • Hebrewwindows-1255, iso-8859-8
  • Arabicwindows-1256, iso-8859-6
  • Turkish, Vietnamese, Thaiwindows-1254, windows-1258, windows-874

/diagnose encodes your suspect text back into bytes with every one of those 28 legacy encoders and reads the result as strict UTF-8. evidence shows the resulting byte trail character by character — Ã0xC3 → "Byte 1 of a 2-byte UTF-8 sequence decoding to U+00E9 'é'" — so you can check the reasoning instead of trusting it. encoding names the culprit, and alternatives lists every rival reading grouped by the text it recovers, so the encodings that merely agree are visibly separated from the one that disagrees. layers (1–3, default 2) controls how many stacked decodes it will unwind, which is what separates café from the double-encoded café. When that ceiling is what stopped the unwind, the answer says so instead of pretending to be finished: complete is false, the confidence never reaches high, and a warning names the cause and how to go further.

/transcode is the generator: ?text=café&to=windows-1252 returns the bytes 63 61 66 C3 A9 and the string café, ready to paste into a test fixture. Deliberate garbling is the half of this problem that repair tools tend to skip, and a repair path you cannot feed broken input to is a repair path you have not tested.

What this endpoint will not claim

  • A diagnosis is a hypothesis, never a verdict. Many byte trails are genuinely ambiguous — é really is a legitimate two-character string, and the Latin-family encodings agree on most of 0xA00xFF, so a dozen of them routinely recover the same text — so every answer carries a confidence of high, medium or low, a confidenceRationale that counts the rivals on both sides, and an alternatives array naming them. high needs two or more UTF-8 sequences, a sequence of three or more bytes, or two stacked layers, and a recovery that no longer unwinds; a single two-byte trail unwound in one layer is only ever medium.
  • Recovery is all or nothing. If any part of the input fails to decode, no repair is offered and hypothesis is none — a partly-repaired string is exactly the kind of plausible wrong answer this API refuses to produce. Half-fixed text such as café naïve lands there: the confidence drops to low and a warning points you at /transcode for the broken fragment. Ordinary text such as café, which simply is not mojibake, is none with high confidence instead. A decode that does succeed is still rejected when it introduces junk the input did not already contain — a U+FFFD replacement character, a C1 control, a Unicode noncharacter or a private-use code point — and the rationale names the exact code point and position it found rather than describing the rule. The one partial answer that is returned is labelled rather than dressed up: a recovery that still unwinds — because layers ran out, or because a different encoding continues the chain — comes back with complete: false, a confidence capped below high, and a warning saying which of the two it is.
  • Single-byte legacy encodings only. Shift_JIS, EUC-JP, EUC-KR, Big5 and GBK/gb18030 are not supported. They are not 128-entry tables: their five WHATWG indexes hold 6,067 to 23,940 mappings each — 73,369 in total, more than 200 KB of source even packed as tightly as base64 allows — and they need a different decoding algorithm plus per-encoding special cases (Shift_JIS's half-width katakana, Big5's two-code-point pointers, gb18030's four-byte ranges). They also break the invariant this endpoint's evidence trail is built on — one byte per character — so shipping them half-implemented would mean a mislabelled byte trail, which is worse than an honest gap. The original text can be any script (Japanese, Chinese, Korean and Cyrillic all recover perfectly from 日本語日本語); it is the encoding that misread it that must be single-byte.
  • iso-8859-1 here means ISO/IEC 8859-1. The Encoding Standard maps the label iso-8859-1 (and latin1, cp819, ascii) onto windows-1252, which is why browsers turn byte 0x92 into rather than a C1 control. This API exposes both behaviours as separately named encodings so you can tell which one your pipeline actually performed. They differ for exactly 27 byte values, all inside 0x800x9F, which windows-1252 fills with printable characters and ISO-8859-1 leaves as C1 controls — which is why one emoji has two different mojibakes: 😀 becomes 😀 through windows-1252 but ð plus three invisible controls through true ISO-8859-1. Encoding labels are deliberately not accepted as values: latin1 and cp1252 both mean windows-1252 to the Standard, which is exactly the confusion this endpoint exists to expose.
  • Characters with no byte are reported, not replaced. /transcode never substitutes ? or U+FFFD on the encode side: has no ISO-8859-1 byte, so it is listed in unmappable and omitted from bytes. from=utf-8 never reports anything, because UTF-8 can represent every Unicode scalar value. On the decode side, to=utf-8 uses the Encoding Standard's replacement error mode — which is what a real system does — and warns you how many decoding errors it replaced. That count excludes any U+FFFD your text genuinely contained, so ?text=caf%EF%BF%BD&to=utf-8 is correctly reported as well-formed.
  • Some bytes genuinely do not decode. 20 of the 28 encodings assign all 128 high bytes, so every one of the 256 byte values decodes. The other 8 leave holes — windows-1253 3, windows-1255 10, windows-1257 2, windows-874 8, iso-8859-7 3, iso-8859-6 45, iso-8859-8 36, iso-8859-3 7 unassigned bytes — and the Encoding Standard's decoder errors on those. /transcode follows the replacement error mode, emitting U+FFFD and warning with the exact byte values, rather than inventing a character for a byte the Standard does not define. That step is lossy: the byte value survives nowhere in the output, so /diagnose answers none for any text containing U+FFFD and says exactly why, instead of guessing what the byte used to be.

Both routes accept up to 2,000 UTF-16 code units and are pure computation: no clock, no randomness, no network. The same query returns the same bytes forever.

Use it for

  • Work out whether a legacy database column was written UTF-8 and read latin1, and recover the original text
  • Recover Russian, Greek or Hebrew text a legacy pipeline turned into привет or Ελλάδα
  • Generate deliberately garbled strings to test an importer's mojibake-repair path
  • Show a support engineer the exact byte trail behind a customer's é or ’ complaint
  • Check whether a character survives a windows-1252 round-trip before exporting a legacy CSV

Frequently asked questions

Why does my database show é instead of é?

Because é was stored as its two UTF-8 bytes 0xC3 0xA9 and then read back one byte at a time as windows-1252, where those bytes are à and ©. /diagnose?text=café shows that byte trail and returns café.

What does ’ mean in my text?

It is a curly apostrophe, U+2019. In UTF-8 that is the three bytes 0xE2 0x80 0x99, which windows-1252 renders as â, and . The same fault turns into “ and into †followed by an invisible control character.

How do I fix Russian text that shows привет?

That is UTF-8 Cyrillic read back as windows-1252 — each two-byte Cyrillic character became Ð or Ñ plus one more character. /diagnose?text=привет returns привет with the byte trail. Text mangled through windows-1251, KOI8-R, IBM866, ISO-8859-5 or x-mac-cyrillic is diagnosed the same way.

Can I generate mojibake on purpose for a test fixture?

Yes — that is what /transcode is for. ?text=café&from=utf-8&to=windows-1252 returns the bytes 63 61 66 C3 A9 and the garbled string café, so you can feed your repair path real broken input.

Which character encodings does it support, and does it handle Shift_JIS or GBK?

All 27 legacy single-byte encodings in the WHATWG Encoding Standard — every windows-125x and ISO-8859 part, KOI8-R/U, IBM866, windows-874, macintosh and x-mac-cyrillic — plus true ISO/IEC 8859-1, which the Standard itself folds into windows-1252. The multi-byte CJK encodings (Shift_JIS, EUC-JP, EUC-KR, Big5, GBK/gb18030) are not supported. Japanese or Chinese text garbled through a single-byte encoding still recovers perfectly.

Does the API guess, or can I check its reasoning?

It reports a hypothesis with a high/medium/low confidence, a written rationale, an evidence array giving every byte and its role in the UTF-8 sequence, and an alternatives array naming every rival encoding grouped by the text it recovers. A single two-byte trail unwound in one layer is never labelled high — and neither is a recovery that still unwinds, which is flagged with complete: false and a warning.

Standards & references