Skip to main content

Punycode and IDN Converter API

REAL DATA

Convert internationalized domains to punycode and back, and get the exact IDNA rule that rejected a label — length, hyphens, combining marks or the bidi rule.

Authoritative reference data or standards computation

Base URL
/api/punycode
Capabilities
3 routes
Last updated
July 30, 2026
Data source

GET /api/punycode/encode

Maps Unicode labels with UTS #46 — an xn-- label is validated without mapping, as the standard requires — then Punycode-encodes only the labels that need it. A label that breaks a rule but can still be converted keeps its A-label, alongside valid=false and a warning; ascii is null only when a label cannot be converted at all.

Live requestRuns against the public API
No key required
GET/api/punycode/encode?domain=m%C3%BCnchen.de

Request parameters

Unicode domain or single label to convert, up to 1024 characters. The three UTS #46 label separators U+3002, U+FF0E and U+FF61 are folded to '.' before splitting.

domain splits the input at dots and treats each part as a label; label treats the whole input as one label, so an embedded dot is reported as a rule violation instead of a separator.

Apply UTS #46 transitional processing (deprecated) to the deviation characters: sharp s becomes ss, final sigma becomes sigma, and the zero-width joiner and non-joiner are removed. Existing xn-- labels are never remapped.

Require every ASCII code point in a label to be a lower-case letter, digit or hyphen. Turn it off to accept underscore labels such as _dmarc that DNS carries but IDNA does not permit. It cannot admit everything: '# % / : < > ? @ [ \ ] ^ |', space, tab, newline and the other C0 controls are forbidden domain code points in the WHATWG URL host parser — the only UTS #46 entry point the runtime offers — and are reported as forbidden-character whatever this flag is set to.

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/punycode/encode?domain=m%C3%BCnchen.de"
const res = await fetch("https://randomapi.dev/api/punycode/encode?domain=m%C3%BCnchen.de");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/punycode/encode?domain=m%C3%BCnchen.de").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/punycode/encode?domain=m%C3%BCnchen.de"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 4
domain string required

Unicode domain or single label to convert, up to 1024 characters. The three UTS #46 label separators U+3002, U+FF0E and U+FF61 are folded to '.' before splitting.

allowed: 1 – 1024
example: domain=münchen.de
mode enum

domain splits the input at dots and treats each part as a label; label treats the whole input as one label, so an embedded dot is reported as a rule violation instead of a separator.

default: domain
allowed: domain | label
example: mode=label
transitional boolean

Apply UTS #46 transitional processing (deprecated) to the deviation characters: sharp s becomes ss, final sigma becomes sigma, and the zero-width joiner and non-joiner are removed. Existing xn-- labels are never remapped.

default: false
example: transitional=true
useStd3AsciiRules boolean

Require every ASCII code point in a label to be a lower-case letter, digit or hyphen. Turn it off to accept underscore labels such as _dmarc that DNS carries but IDNA does not permit. It cannot admit everything: '# % / : < > ? @ [ \ ] ^ |', space, tab, newline and the other C0 controls are forbidden domain code points in the WHATWG URL host parser — the only UTS #46 entry point the runtime offers — and are reported as forbidden-character whatever this flag is set to.

default: true
example: useStd3AsciiRules=false
Universal parameters Shared response and formatting options 4
fields list

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

example: fields=input,ascii
exclude list

Return all fields except these (comma-separated).

example: exclude=labels
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 domain exactly as supplied, after surrounding whitespace is trimmed.

example: münchen.de

ascii string nullable

ASCII (A-label) form, with 'xn--' added only to labels containing non-ASCII code points. Null when a label could not be converted at all: it holds a code point the URL host parser forbids, the runtime refused to map it, or it is an xn-- label that does not decode.

example: xn--mnchen-3ya.de

unicode string

Unicode (U-label) form: labels supplied as Unicode after UTS #46 mapping (lower-cased, NFC-normalized, compatibility characters folded), and labels supplied as xn-- shown as their decoded content, which the standard validates without mapping and so is not re-normalized.

example: münchen.de

isIdn boolean

True when at least one label needs Punycode.

example: true

asciiLength integer nullable

Byte length of the ASCII form excluding the DNS root label and its dot; the limit is 253. Null when ascii is null.

example: 17

valid boolean

True when every rule this API checks passes. Call /analyze for the rules that failed.

example: true

labels object[]

Per-label conversion: index, input, unicode, ascii, isAce, isIdn, asciiLength and valid.

example: [{"index":0,"input":"münchen","unicode":"münchen","ascii":"xn--mnchen-3ya","isAce":false,"isIdn":true,"asciiLength":14,"valid":true}]

Documented examples

Build-generated requests and complete responses
4
Encode a German domain
GET /api/punycode/encode?domain=m%C3%BCnchen.de
{
  "data": {
    "input": "münchen.de",
    "ascii": "xn--mnchen-3ya.de",
    "unicode": "münchen.de",
    "isIdn": true,
    "asciiLength": 17,
    "valid": true,
    "labels": [
      {
        "index": 0,
        "input": "münchen",
        "unicode": "münchen",
        "ascii": "xn--mnchen-3ya",
        "isAce": false,
        "isIdn": true,
        "asciiLength": 14,
        "valid": true
      },
      {
        "index": 1,
        "input": "de",
        "unicode": "de",
        "ascii": "de",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 2,
        "valid": true
      }
    ]
  },
  "meta": {
    "endpoint": "punycode",
    "route": "/encode",
    "params": {
      "domain": "münchen.de",
      "mode": "domain",
      "transitional": false,
      "useStd3AsciiRules": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Transitional processing turns sharp s into ss
GET /api/punycode/encode?domain=fa%C3%9F.de&transitional=true
{
  "data": {
    "input": "faß.de",
    "ascii": "fass.de",
    "unicode": "fass.de",
    "isIdn": false,
    "asciiLength": 7,
    "valid": true,
    "labels": [
      {
        "index": 0,
        "input": "faß",
        "unicode": "fass",
        "ascii": "fass",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 4,
        "valid": true
      },
      {
        "index": 1,
        "input": "de",
        "unicode": "de",
        "ascii": "de",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 2,
        "valid": true
      }
    ]
  },
  "meta": {
    "endpoint": "punycode",
    "route": "/encode",
    "params": {
      "domain": "faß.de",
      "mode": "domain",
      "transitional": true,
      "useStd3AsciiRules": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Encode one Japanese label
GET /api/punycode/encode?domain=%E6%97%A5%E6%9C%AC%E8%AA%9E&mode=label
{
  "data": {
    "input": "日本語",
    "ascii": "xn--wgv71a119e",
    "unicode": "日本語",
    "isIdn": true,
    "asciiLength": 14,
    "valid": true,
    "labels": [
      {
        "index": 0,
        "input": "日本語",
        "unicode": "日本語",
        "ascii": "xn--wgv71a119e",
        "isAce": false,
        "isIdn": true,
        "asciiLength": 14,
        "valid": true
      }
    ]
  },
  "meta": {
    "endpoint": "punycode",
    "route": "/encode",
    "params": {
      "domain": "日本語",
      "mode": "label",
      "transitional": false,
      "useStd3AsciiRules": true
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Allow an underscore label
GET /api/punycode/encode?domain=_dmarc.example.com&useStd3AsciiRules=false
{
  "data": {
    "input": "_dmarc.example.com",
    "ascii": "_dmarc.example.com",
    "unicode": "_dmarc.example.com",
    "isIdn": false,
    "asciiLength": 18,
    "valid": true,
    "labels": [
      {
        "index": 0,
        "input": "_dmarc",
        "unicode": "_dmarc",
        "ascii": "_dmarc",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 6,
        "valid": true
      },
      {
        "index": 1,
        "input": "example",
        "unicode": "example",
        "ascii": "example",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 7,
        "valid": true
      },
      {
        "index": 2,
        "input": "com",
        "unicode": "com",
        "ascii": "com",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 3,
        "valid": true
      }
    ]
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/punycode/decode

Pure RFC 3492 decoding: no UTS #46 mapping beyond ASCII case folding, and no validity checking. roundTripsExactly is false when re-encoding the decoded label does not reproduce the supplied A-label byte for byte.

Live requestRuns against the public API
No key required
GET/api/punycode/decode?domain=xn--mnchen-3ya.de

Request parameters

ASCII domain or single label to decode, up to 1024 characters. Non-ASCII input is a 400 — use /encode for that.

domain splits the input at dots and treats each part as a label; label treats the whole input as one label, so an embedded dot is reported as a rule violation instead of a separator.

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/punycode/decode?domain=xn--mnchen-3ya.de"
const res = await fetch("https://randomapi.dev/api/punycode/decode?domain=xn--mnchen-3ya.de");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/punycode/decode?domain=xn--mnchen-3ya.de").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/punycode/decode?domain=xn--mnchen-3ya.de"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 2
domain string required

ASCII domain or single label to decode, up to 1024 characters. Non-ASCII input is a 400 — use /encode for that.

allowed: 1 – 1024
example: domain=xn--mnchen-3ya.de
mode enum

domain splits the input at dots and treats each part as a label; label treats the whole input as one label, so an embedded dot is reported as a rule violation instead of a separator.

default: domain
allowed: domain | label
example: mode=label
Universal parameters Shared response and formatting options 4
fields list

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

example: fields=input,ascii
exclude list

Return all fields except these (comma-separated).

example: exclude=labels
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 6
input string

The domain exactly as supplied, after surrounding whitespace is trimmed.

example: xn--mnchen-3ya.de

ascii string

The input lower-cased, which is the canonical case for A-labels in the DNS.

example: xn--mnchen-3ya.de

unicode string

Decoded U-label form; a label that fails to decode is left unchanged.

example: münchen.de

isIdn boolean

True when at least one label carried the xn-- ACE prefix and decoded successfully.

example: true

roundTripsExactly boolean

True when re-encoding every decoded label with RFC 3492 reproduces the supplied A-label exactly.

example: true

labels object[]

Per-label decoding: index, ascii, unicode, isAce, isIdn, roundTripsExactly, canonicalAscii (the form a conforming encoder would produce) and error.

example: [{"index":0,"ascii":"xn--mnchen-3ya","unicode":"münchen","isAce":true,"isIdn":true,"roundTripsExactly":true,"canonicalAscii":"xn--mnchen-3ya","error":null}]

Documented examples

Build-generated requests and complete responses
4
Decode a German A-label
GET /api/punycode/decode?domain=xn--mnchen-3ya.de
{
  "data": {
    "input": "xn--mnchen-3ya.de",
    "ascii": "xn--mnchen-3ya.de",
    "unicode": "münchen.de",
    "isIdn": true,
    "roundTripsExactly": true,
    "labels": [
      {
        "index": 0,
        "ascii": "xn--mnchen-3ya",
        "unicode": "münchen",
        "isAce": true,
        "isIdn": true,
        "roundTripsExactly": true,
        "canonicalAscii": "xn--mnchen-3ya",
        "error": null
      },
      {
        "index": 1,
        "ascii": "de",
        "unicode": "de",
        "isAce": false,
        "isIdn": false,
        "roundTripsExactly": true,
        "canonicalAscii": null,
        "error": null
      }
    ]
  },
  "meta": {
    "endpoint": "punycode",
    "route": "/decode",
    "params": {
      "domain": "xn--mnchen-3ya.de",
      "mode": "domain"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Decode a Japanese A-label
GET /api/punycode/decode?domain=xn--wgv71a119e.jp
{
  "data": {
    "input": "xn--wgv71a119e.jp",
    "ascii": "xn--wgv71a119e.jp",
    "unicode": "日本語.jp",
    "isIdn": true,
    "roundTripsExactly": true,
    "labels": [
      {
        "index": 0,
        "ascii": "xn--wgv71a119e",
        "unicode": "日本語",
        "isAce": true,
        "isIdn": true,
        "roundTripsExactly": true,
        "canonicalAscii": "xn--wgv71a119e",
        "error": null
      },
      {
        "index": 1,
        "ascii": "jp",
        "unicode": "jp",
        "isAce": false,
        "isIdn": false,
        "roundTripsExactly": true,
        "canonicalAscii": null,
        "error": null
      }
    ]
  },
  "meta": {
    "endpoint": "punycode",
    "route": "/decode",
    "params": {
      "domain": "xn--wgv71a119e.jp",
      "mode": "domain"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Non-canonical punycode is reported
GET /api/punycode/decode?domain=xn--mnchen-3ya-.example
{
  "data": {
    "input": "xn--mnchen-3ya-.example",
    "ascii": "xn--mnchen-3ya-.example",
    "unicode": "mnchen-3ya.example",
    "isIdn": true,
    "roundTripsExactly": false,
    "labels": [
      {
        "index": 0,
        "ascii": "xn--mnchen-3ya-",
        "unicode": "mnchen-3ya",
        "isAce": true,
        "isIdn": true,
        "roundTripsExactly": false,
        "canonicalAscii": "mnchen-3ya",
        "error": null
      },
      {
        "index": 1,
        "ascii": "example",
        "unicode": "example",
        "isAce": false,
        "isIdn": false,
        "roundTripsExactly": true,
        "canonicalAscii": null,
        "error": null
      }
    ]
  },
  "meta": {
    "endpoint": "punycode",
    "route": "/decode",
    "params": {
      "domain": "xn--mnchen-3ya-.example",
      "mode": "domain"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z",
    "warnings": [
      "At least one label is not canonical RFC 3492 punycode; see labels[].canonicalAscii and labels[].error."
    ]
  }
}
Decode one label
GET /api/punycode/decode?domain=xn--wgv71a119e&mode=label
{
  "data": {
    "input": "xn--wgv71a119e",
    "ascii": "xn--wgv71a119e",
    "unicode": "日本語",
    "isIdn": true,
    "roundTripsExactly": true,
    "labels": [
      {
        "index": 0,
        "ascii": "xn--wgv71a119e",
        "unicode": "日本語",
        "isAce": true,
        "isIdn": true,
        "roundTripsExactly": true,
        "canonicalAscii": "xn--wgv71a119e",
        "error": null
      }
    ]
  },
  "meta": {
    "endpoint": "punycode",
    "route": "/decode",
    "params": {
      "domain": "xn--wgv71a119e",
      "mode": "label"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/punycode/analyze

Runs every rule this API implements and returns one failure entry per violation, each citing the RFC or UTS #46 clause it comes from.

Live requestRuns against the public API
No key required
GET/api/punycode/analyze?domain=m%C3%BCnchen.de

Request parameters

Domain to analyze, up to 1024 characters. Unicode labels and xn-- labels may be mixed in the same name.

Apply UTS #46 transitional processing (deprecated) to the deviation characters: sharp s becomes ss, final sigma becomes sigma, and the zero-width joiner and non-joiner are removed. Existing xn-- labels are never remapped.

Require every ASCII code point in a label to be a lower-case letter, digit or hyphen. Turn it off to accept underscore labels such as _dmarc that DNS carries but IDNA does not permit. It cannot admit everything: '# % / : < > ? @ [ \ ] ^ |', space, tab, newline and the other C0 controls are forbidden domain code points in the WHATWG URL host parser — the only UTS #46 entry point the runtime offers — and are reported as forbidden-character whatever this flag is set to.

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/punycode/analyze?domain=m%C3%BCnchen.de"
const res = await fetch("https://randomapi.dev/api/punycode/analyze?domain=m%C3%BCnchen.de");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/punycode/analyze?domain=m%C3%BCnchen.de").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/punycode/analyze?domain=m%C3%BCnchen.de"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 3
domain string required

Domain to analyze, up to 1024 characters. Unicode labels and xn-- labels may be mixed in the same name.

allowed: 1 – 1024
example: domain=münchen.de
transitional boolean

Apply UTS #46 transitional processing (deprecated) to the deviation characters: sharp s becomes ss, final sigma becomes sigma, and the zero-width joiner and non-joiner are removed. Existing xn-- labels are never remapped.

default: false
example: transitional=true
useStd3AsciiRules boolean

Require every ASCII code point in a label to be a lower-case letter, digit or hyphen. Turn it off to accept underscore labels such as _dmarc that DNS carries but IDNA does not permit. It cannot admit everything: '# % / : < > ? @ [ \ ] ^ |', space, tab, newline and the other C0 controls are forbidden domain code points in the WHATWG URL host parser — the only UTS #46 entry point the runtime offers — and are reported as forbidden-character whatever this flag is set to.

default: true
example: useStd3AsciiRules=false
Universal parameters Shared response and formatting options 4
fields list

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

example: fields=input,ascii
exclude list

Return all fields except these (comma-separated).

example: exclude=failures
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 9
input string

The domain exactly as supplied, after surrounding whitespace is trimmed.

example: münchen.de

ascii string nullable

ASCII (A-label) form, or null when a label could not be converted at all. A label that merely breaks a rule — including the bidi rule — still gets its A-label.

example: xn--mnchen-3ya.de

unicode string

Unicode (U-label) form after mapping and Punycode decoding. For a label the runtime refused to map, this is a best-effort case-folded NFC form and the failures array says so with uts46-processing.

example: münchen.de

valid boolean

True when the failures array is empty.

example: true

isIdn boolean

True when at least one label needs Punycode.

example: true

isBidiDomain boolean

True when some label contains a right-to-left character, which is what makes the RFC 5893 Bidi Rule apply to every label in the name.

example: false

asciiLength integer nullable

Byte length of the ASCII form excluding the DNS root label and its dot; the limit is 253.

example: 17

labels object[]

Per-label result: index, input, unicode, ascii, isAce, isIdn, asciiLength, valid and direction — rtl when the label contains an R, AL or AN character (RFC 5893 section 1.4), which is the test that makes the whole name a bidi domain, not the first-character test that picks which conditions apply.

example: [{"index":0,"input":"münchen","unicode":"münchen","ascii":"xn--mnchen-3ya","isAce":false,"isIdn":true,"asciiLength":14,"valid":true,"direction":"ltr"}]

failures object[]

One entry per violated rule: label, labelIndex (null for whole-name rules), rule and a detail sentence naming the clause it comes from. Empty when the domain is valid.

example: [{"label":"ab--cd","labelIndex":0,"rule":"hyphen-3-4","detail":"A label must not contain '--' in the third and fourth character positions; those positions are reserved for ACE prefixes such as 'xn--' (RFC 5891 section 4.2.3.1)."}]

Documented examples

Build-generated requests and complete responses
4
A valid German IDN
GET /api/punycode/analyze?domain=m%C3%BCnchen.de
{
  "data": {
    "input": "münchen.de",
    "ascii": "xn--mnchen-3ya.de",
    "unicode": "münchen.de",
    "valid": true,
    "isIdn": true,
    "isBidiDomain": false,
    "asciiLength": 17,
    "labels": [
      {
        "index": 0,
        "input": "münchen",
        "unicode": "münchen",
        "ascii": "xn--mnchen-3ya",
        "isAce": false,
        "isIdn": true,
        "asciiLength": 14,
        "valid": true,
        "direction": "ltr"
      },
      {
        "index": 1,
        "input": "de",
        "unicode": "de",
        "ascii": "de",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 2,
        "valid": true,
        "direction": "ltr"
      }
    ],
    "failures": []
  },
  "meta": {
    "endpoint": "punycode",
    "route": "/analyze",
    "params": {
      "domain": "münchen.de",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Mixed-direction label fails the bidi rule
GET /api/punycode/analyze?domain=abc%D8%B9%D8%B1%D8%A8%D9%8A.example
{
  "data": {
    "input": "abcعربي.example",
    "ascii": "xn--abc-uze8b7bxh.example",
    "unicode": "abcعربي.example",
    "valid": false,
    "isIdn": true,
    "isBidiDomain": true,
    "asciiLength": 25,
    "labels": [
      {
        "index": 0,
        "input": "abcعربي",
        "unicode": "abcعربي",
        "ascii": "xn--abc-uze8b7bxh",
        "isAce": false,
        "isIdn": true,
        "asciiLength": 17,
        "valid": false,
        "direction": "rtl"
      },
      {
        "index": 1,
        "input": "example",
        "unicode": "example",
        "ascii": "example",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 7,
        "valid": true,
        "direction": "ltr"
      }
    ],
    "failures": [
      {
        "label": "abcعربي",
        "labelIndex": 0,
        "rule": "bidi-rule",
        "detail": "RFC 5893 condition 5: U+0639 has Bidi property AL, which an LTR label may not contain."
      },
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Hyphens in the third and fourth positions
GET /api/punycode/analyze?domain=ab--cd.example
{
  "data": {
    "input": "ab--cd.example",
    "ascii": "ab--cd.example",
    "unicode": "ab--cd.example",
    "valid": false,
    "isIdn": false,
    "isBidiDomain": false,
    "asciiLength": 14,
    "labels": [
      {
        "index": 0,
        "input": "ab--cd",
        "unicode": "ab--cd",
        "ascii": "ab--cd",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 6,
        "valid": false,
        "direction": "ltr"
      },
      {
        "index": 1,
        "input": "example",
        "unicode": "example",
        "ascii": "example",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 7,
        "valid": true,
        "direction": "ltr"
      }
    ],
    "failures": [
      {
        "label": "ab--cd",
        "labelIndex": 0,
        "rule": "hyphen-3-4",
        "detail": "A label must not contain '--' in the third and fourth character positions; those positions are reserved for ACE prefixes such as 'xn--' (RFC 5891 section 4.2.3.1)."
      }
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
An underscore label DNS allows but IDNA does not
GET /api/punycode/analyze?domain=_dmarc.example.com
{
  "data": {
    "input": "_dmarc.example.com",
    "ascii": "_dmarc.example.com",
    "unicode": "_dmarc.example.com",
    "valid": false,
    "isIdn": false,
    "isBidiDomain": false,
    "asciiLength": 18,
    "labels": [
      {
        "index": 0,
        "input": "_dmarc",
        "unicode": "_dmarc",
        "ascii": "_dmarc",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 6,
        "valid": false,
        "direction": "ltr"
      },
      {
        "index": 1,
        "input": "example",
        "unicode": "example",
        "ascii": "example",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 7,
        "valid": true,
        "direction": "ltr"
      },
      {
        "index": 2,
        "input": "com",
        "unicode": "com",
        "ascii": "com",
        "isAce": false,
        "isIdn": false,
        "asciiLength": 3,
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

About this API

Coverage & behavior

A complete RFC 3492 Punycode codec plus the label rules that decide whether an internationalized domain name is legal — and, when it is not, which rule rejected it.

  • /encode converts a Unicode domain to its ASCII (A-label) form. Only labels containing non-ASCII code points receive the xn-- prefix; plain ASCII labels are lower-cased and passed through.
  • /decode converts xn-- labels back to Unicode with this API's own RFC 3492 decoder and reports roundTripsExactly, which catches non-canonical punycode: strings that decode successfully but are not the encoding a conforming encoder would produce. In practice almost every real hit is an A-label whose content turns out to be pure ASCII (xn--ab- decodes to ab, which must never carry the prefix) or one carrying a redundant delimiter; the check is a byte comparison against this module's own encoder, so it catches any deviation, but do not expect it to flag much beyond those.
  • /analyze is the explain route. It returns a failures array of { label, labelIndex, rule, detail } naming every rule that failed, drawn from this vocabulary: empty-label, label-too-long, domain-too-long, label-contains-dot, not-nfc, hyphen-3-4, hyphen-start-end, leading-combining-mark, std3-ascii-rules, bidi-rule, forbidden-character, punycode-decode, non-canonical-punycode and uts46-processing.

What is computed here, and what is delegated

The Punycode codec (RFC 3492), the DNS limits of 63 bytes per label and 253 bytes per name, the hyphen restrictions and the leading-combining-mark rule of RFC 5891 section 4.2.3, the NFC check, the STD3 ASCII rules and all six numbered conditions of the RFC 5893 Bidi Rule are implemented in this endpoint from the specification text. Condition 1 is applied as written: the label's own first character decides whether the rest is scored as an RTL label (conditions 2, 3, 4) or an LTR label (conditions 5, 6), while "contains an R, AL or AN character anywhere" is the separate section 1.4 test for whether the name is a Bidi domain name at all. The Bidi Rule needs the Unicode Bidi_Class property, which JavaScript does not expose through regular-expression property escapes, so a pinned Unicode 17.0.0 extract of DerivedBidiClass.txt ships with the Worker. The NFC and General_Category=Mark tests use the runtime's Unicode data, because that is what String.normalize and \p{M} are.

Code-point mapping and repertoire — which characters are valid, ignored, mapped or disallowed — is performed by the serving runtime's own UTS #46 implementation through the WHATWG URL host parser, exactly as a browser does it. IdnaMappingTable.txt is over 750 KB of source data covering every code point, and is deliberately not bundled, so mapping results follow the runtime's Unicode version and can shift when the runtime is upgraded; no frozen Unicode version is claimed for that step. The runtime also applies the CONTEXTJ joiner rules.

That parser insists on running the Bidi Rule itself, and refuses to hand back an ASCII form for a label that breaks it. Since mapping is per-code-point and normalization is closed under concatenation (UAX #15), such a label is mapped in contiguous pieces cut at the boundaries between right-to-left and other characters, and reassembled — so a name like abcعربي.example still reports its A-label xn--abc-uze8b7bxh.example next to valid: false. The pieces come from the runtime, never from a guess: if the runtime refuses one of them too, ascii stays null, the rule is reported as uts46-processing, and the rules that read ASCII positions out of a mapped label (hyphen-3-4, hyphen-start-end, std3-ascii-rules) are not scored at all, because the string they would be scored on is not the mapped one.

Deliberate limits

  • Not a homograph or confusable check. Detecting lookalike domains is UTS #39, a different standard with a different data set. This endpoint does not attempt it, and valid: true says nothing about whether a name is visually confusable with another — that question belongs to /api/confusables, which implements the UTS #39 skeleton and mixed-script tests.
  • No DNS lookup, ever. A valid IDN is not a registered, delegated or resolvable domain.
  • No registry policy. Per-TLD script restrictions, Label Generation Rulesets and variant tables are out of scope; a registry may refuse a name this API considers valid.
  • UTS #46 repertoire, not IDNA2008's. Characters marked NV8 — valid under UTS #46 but excluded from IDNA2008, such as U+2665 (♥) — are accepted here because the runtime accepts them. The stricter IDNA2008 derived-property table is not bundled.
  • A handful of ASCII code points never get through, whatever useStd3AsciiRules says: # % / : < > ? @ [ \ ] ^ |, space, tab, newline and the other C0 controls are forbidden domain code points in the WHATWG URL host parser, which is the only UTS #46 entry point the runtime offers. They are reported as forbidden-character rather than std3-ascii-rules. (< and > composed with U+0338 into ≮ and ≯ are ordinary valid characters and do pass.)
  • Input is capped at 1024 characters and 128 labels, and an empty value is a 400.

Use it for

  • Convert a Unicode domain to its xn-- A-label before writing a DNS zone file or issuing a certificate
  • Tell a user exactly which IDNA rule rejected the domain they typed instead of a bare 'invalid domain'
  • Decode xn-- hostnames in access logs and abuse reports back into readable Unicode
  • Regression-test a punycode implementation against RFC 3492's own published sample strings

Frequently asked questions

How do I convert a Unicode domain to punycode?

Call /api/punycode/encode?domain=münchen.de and read ascii, which is xn--mnchen-3ya.de. Only labels containing non-ASCII code points get the xn-- prefix.

Why is my IDN domain invalid?

/api/punycode/analyze answers exactly that: failures names each broken rule — hyphen-3-4, leading-combining-mark, label-too-long, bidi-rule and the rest — with a detail sentence citing the clause it comes from.

What is the IDNA bidi rule and when does it fail?

One right-to-left character anywhere in a name makes it a Bidi domain name, and RFC 5893's six conditions then apply to every label in it. Each label's own first character picks the conditions: abcعربي starts with a Latin letter, so it is an LTR label and its Arabic letters break conditions 5 and 6, while 1عربي starts with a digit and breaks condition 1.

Does the API detect homograph or lookalike domains?

No — this endpoint only answers whether a name is well-formed under IDNA. Confusable detection is Unicode Technical Standard #39, a separate standard with its own data, and it lives at /api/confusables.

Does a valid result mean the domain is registered?

No. No DNS query is made at any point, and registry rules such as per-TLD script restrictions are out of scope, so a valid name may still be unregistrable or unresolvable.

What does transitional processing change?

It maps the UTS #46 deviation characters, so faß.de encodes to fass.de instead of xn--fa-hia.de. It is deprecated, defaults to false, and never remaps a label that is already in xn-- form.

Standards & references