API reference

Parse a URL

Split a URL into its parts with the WHATWG URL parser browsers use.

GET POST /api/url/parse MCP tool url_parse

Parses a URL exactly as browsers and Cloudflare Workers do (WHATWG URL Standard) and returns every component: scheme, username, host in ASCII and Unicode forms, port and default port, origin, path segments decoded, query parameters as an ordered list (duplicates kept), and fragment. It classifies the host (domain, IPv4, IPv6, localhost, private or other special-purpose range) without resolving DNS. A password, if present, is never returned: hasPassword is true and href shows *** instead. With base, a relative reference is resolved first.

Browser version (runs locally): URL Parser.

Try it

Credentials
Remember these credentials

Not saved: you will paste them again next time.

No credentials yet? Get a client ID and secret. They are sent only to this site, in an Authorization: Basic header.

POST /api/url/parse with a JSON body

Parameters

Parameter Type Default Description
input required string (≤ 16384 chars) — The URL, absolute (https://…), or relative (../a?b=1) when base is given.
base string (≤ 16384 chars) — Absolute base URL for resolving a relative input (RFC 3986 §5 as refined by WHATWG). Ignored when input is absolute.

Also accepted on every endpoint: format (json | text). Parameters may come from the query string, a JSON object body, an urlencoded or multipart form, or (for input) a text/plain body.

Global parameters
Parameter Type Default Description
format json | text json json returns the documented object. text returns only the primary value as text/plain (see "Text output").

Response 200 application/json

Field Type Description
href string The normalized URL (WHATWG serialization). If the input had a password, it is replaced by ***.
hrefRedacted boolean True when href had its password replaced.
resolvedAgainstBase boolean True when the input was relative and was resolved against base.
protocol string Scheme with the colon, e.g. https:.
scheme string Scheme without the colon, lowercase.
special boolean A WHATWG special scheme (http, https, ws, wss, ftp, file).
username object
username.raw string As serialized in the normalized URL (percent-encoded).
username.decoded string | null Percent-decoded as UTF-8; null if an escape is not valid UTF-8.
hasPassword boolean True when the URL contains a password. The password is never returned.
host string Host plus :port when a non-default port is present (ASCII).
hostname string Host without port, ASCII (Punycode) form. IPv6 includes brackets.
hostnameUnicode string Host with xn-- labels decoded to Unicode (equal to hostname otherwise).
hostType "domain" | "ipv4" | "ipv6" | "opaque" | "empty" | "none" domain, ipv4, ipv6, opaque (host of a non-special scheme), empty (e.g. file:///), or none (no authority, e.g. mailto:).
idn boolean True when any host label is an xn-- (internationalized) label.
ip object | null
ip.version integer 4 or 6.
ip.address string Address as normalized by the URL parser (no brackets).
ip.expanded string IPv6: all eight 4-digit groups. IPv4: dotted decimal.
ip.range "public" | "private" | "loopback" | "link-local" | "unspecified" | "shared" | "multicast" | "broadcast" | "documentation" | "benchmarking" | "reserved" | "unique-local" | "ipv4-mapped" | "nat64" | "site-local" Special-purpose range (IANA registries), or public.
ip.mappedIpv4 string | null Embedded IPv4 address for ::ffff:0:0/96 and 64:ff9b::/96.
isLocalhost boolean localhost, *.localhost (RFC 6761), or a loopback IP.
isPrivate boolean IP in RFC 1918, RFC 6598 shared (100.64/10), fc00::/7 or fec0::/10 (including IPv4-mapped forms).
isPublic boolean | null True for a global unicast IP; false for any special-purpose IP or localhost; null for a domain name (it is not resolved).
port integer | null Explicit non-default port, or null.
defaultPort integer | null Default port of the scheme (http 80, https 443, ws 80, wss 443, ftp 21).
effectivePort integer | null port ?? defaultPort: the port a client would connect to.
origin string | null scheme://host[:port]; null for opaque origins (file:, data:, mailto:, …).
pathname string Path as serialized (percent-encoded).
opaquePath boolean True for URLs like mailto:a@b whose path is not a /-separated list.
pathSegments array of object Path segments split on /; empty for opaque paths.
pathSegments[].raw string As serialized in the normalized URL (percent-encoded).
pathSegments[].decoded string | null Percent-decoded as UTF-8; null if an escape is not valid UTF-8.
search string Query string including ?, or empty.
query array of object Query parameters in order. Duplicate names are kept as separate entries.
query[].name string Decoded name (application/x-www-form-urlencoded: + is a space).
query[].value string Decoded value; empty string when there is no =.
query[].rawName string Name as it appears in the URL.
query[].rawValue string | null Value as it appears in the URL; null when there is no =.
hash string Fragment including #, or empty.
fragment object | null
fragment.raw string As serialized in the normalized URL (percent-encoded).
fragment.decoded string | null Percent-decoded as UTF-8; null if an escape is not valid UTF-8.
warnings array of string Normalizations applied and risks found.

Text output (format=text, text/plain): The normalized href (password redacted).

Errors

Errors are JSON: {"error": "<message>", "code": "<code>", "param": "<name>"?}.

Status Code When
422 unprocessable_input input is not a valid URL (the message suggests the likely cause: no scheme, bad host character, bad port), or base is not a valid absolute URL.
400 missing_parameter A required parameter is absent.
400 unknown_parameter A parameter name is not in the table above (typos are rejected, not ignored).
400 invalid_parameter A value has the wrong type, is out of range, or is not an allowed value.
413 payload_too_large The request body exceeds 1 MiB.
405 method_not_allowed A method other than GET, POST or OPTIONS.

Behaviour

  • Idempotent: the same arguments always return the same result.
  • Requires a client ID and secret (authentication). Each call is recorded as one metadata row against your client ID; inputs and outputs are never stored or logged. Responses carry cache-control: no-store.
  • CORS: any origin may call this endpoint from a browser, using your credentials.

Examples

Responses below were produced by running each request through this code at build time. Long strings are shortened.

URL with duplicate query parameters and an encoded path

curl 'https://util.caruana.me/api/url/parse?input=https%3A%2F%2Fuser%40Example.COM%3A443%2Fa%2520b%2Fc%3Ftag%3Dx%26tag%3Dy%26q%3Dcaf%25C3%25A9%2Bau%2Blait%23top'

Response 200

{
  "href": "https://[email protected]/a%20b/c?tag=x&tag=y&q=caf%C3%A9+au+lait#top",
  "hrefRedacted": false,
  "resolvedAgainstBase": false,
  "protocol": "https:",
  "scheme": "https",
  "special": true,
  "username": {
    "raw": "user",
    "decoded": "user"
  },
  "hasPassword": false,
  "host": "example.com",
  "hostname": "example.com",
  "hostnameUnicode": "example.com",
  "hostType": "domain",
  "idn": false,
  "ip": null,
  "isLocalhost": false,
  "isPrivate": false,
  "isPublic": null,
  "port": null,
  "defaultPort": 443,
  "effectivePort": 443,
  "origin": "https://example.com",
  "pathname": "/a%20b/c",
  "opaquePath": false,
  "pathSegments": [
    {
      "raw": "a%20b",
      "decoded": "a b"
    },
    {
      "raw": "c",
      "decoded": "c"
    }
  ],
  "search": "?tag=x&tag=y&q=caf%C3%A9+au+lait",
  "query": [
    {
      "name": "tag",
      "value": "x",
      "rawName": "tag",
      "rawValue": "x"
    },
    {
      "name": "tag",
      "value": "y",
      "rawName": "tag",
      "rawValue": "y"
    },
    {
      "name": "q",
      "value": "café au lait",
      "rawName": "q",
      "rawValue": "caf%C3%A9+au+lait"
    }
  ],
  "hash": "#top",
  "fragment": {
    "raw": "top",
    "decoded": "top"
  },
  "warnings": [
    "The URL contains a username (userinfo).",
    "The explicit port 443 is the default for https: and was removed from the normalized URL."
  ]
}
Same call as an MCP tools/call
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "url_parse",
    "arguments": {
      "input": "https://[email protected]:443/a%20b/c?tag=x&tag=y&q=caf%C3%A9+au+lait#top"
    }
  }
}

Resolve a relative reference

curl 'https://util.caruana.me/api/url/parse?input=..%2Fimg%2Flogo.png%3Fv%3D2&base=https%3A%2F%2Fexample.com%2Fdocs%2Fguide%2F'

Response 200

{
  "href": "https://example.com/docs/img/logo.png?v=2",
  "hrefRedacted": false,
  "resolvedAgainstBase": true,
  "protocol": "https:",
  "scheme": "https",
  "special": true,
  "username": {
    "raw": "",
    "decoded": ""
  },
  "hasPassword": false,
  "host": "example.com",
  "hostname": "example.com",
  "hostnameUnicode": "example.com",
  "hostType": "domain",
  "idn": false,
  "ip": null,
  "isLocalhost": false,
  "isPrivate": false,
  "isPublic": null,
  "port": null,
  "defaultPort": 443,
  "effectivePort": 443,
  "origin": "https://example.com",
  "pathname": "/docs/img/logo.png",
  "opaquePath": false,
  "pathSegments": [
    {
      "raw": "docs",
      "decoded": "docs"
    },
    {
      "raw": "img",
      "decoded": "img"
    },
    {
      "raw": "logo.png",
      "decoded": "logo.png"
    }
  ],
  "search": "?v=2",
  "query": [
    {
      "name": "v",
      "value": "2",
      "rawName": "v",
      "rawValue": "2"
    }
  ],
  "hash": "",
  "fragment": null,
  "warnings": []
}
Same call as an MCP tools/call
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "url_parse",
    "arguments": {
      "input": "../img/logo.png?v=2",
      "base": "https://example.com/docs/guide/"
    }
  }
}

Private IPv6 host with a non-default port

curl 'https://util.caruana.me/api/url/parse?input=http%3A%2F%2F%5Bfd00%3A%3A1%5D%3A8080%2Fadmin'

Response 200

{
  "href": "http://[fd00::1]:8080/admin",
  "hrefRedacted": false,
  "resolvedAgainstBase": false,
  "protocol": "http:",
  "scheme": "http",
  "special": true,
  "username": {
    "raw": "",
    "decoded": ""
  },
  "hasPassword": false,
  "host": "[fd00::1]:8080",
  "hostname": "[fd00::1]",
  "hostnameUnicode": "[fd00::1]",
  "hostType": "ipv6",
  "idn": false,
  "ip": {
    "version": 6,
    "address": "fd00::1",
    "expanded": "fd00:0000:0000:0000:0000:0000:0000:0001",
    "range": "unique-local",
    "mappedIpv4": null
  },
  "isLocalhost": false,
  "isPrivate": true,
  "isPublic": false,
  "port": 8080,
  "defaultPort": 80,
  "effectivePort": 8080,
  "origin": "http://[fd00::1]:8080",
  "pathname": "/admin",
  "opaquePath": false,
  "pathSegments": [
    {
      "raw": "admin",
      "decoded": "admin"
    }
  ],
  "search": "",
  "query": [],
  "hash": "",
  "fragment": null,
  "warnings": []
}
Same call as an MCP tools/call
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "url_parse",
    "arguments": {
      "input": "http://[fd00::1]:8080/admin"
    }
  }
}