Skip to content

URL Encoder / Decoder · Encoding

URL Encoder / Decoder & Query-String Parser

Percent-encode a value the way its position actually requires, decode one that came back mangled, or paste a whole URL and read every query parameter with its raw text beside it. Double-encoding, + versus %20, punycode hosts and repeated keys are all called out by name — client-side, with nothing uploaded.

Runs in your browser — nothing you paste leaves this page. How we prove that

Runs in your browser RFC 3986 per component No signup Updated Jul 30, 2026

URL encoder, decoder and query-string parser playground

Examples
Mode
Options

Paste a full URL, a bare query string starting with ?, or a form body like a=1&b=2. Nothing is uploaded — the whole thing runs in this tab, so query strings carrying tokens stay yours.

Results update as you type — press Enter to run now.

Result

Paste a URL to see its components and every query parameter decoded — or switch to Decode or Encode to work on one value at a time.

The Gap

One button, one hidden guess about where the value goes.

Almost every “URL encode” box on the web is a single button wired to encodeURIComponent(), and it never says so. That is the right rule for one query value and the wrong rule for a URL you already assembled — feed it a link and every : and / comes back as %3A and %2F. Encode twice by accident and you get %2520, the bug that breaks OAuth redirect_uri round trips and 404s a webhook callback at 3am.

This tool makes the choice explicit instead of hiding it. Component or whole-URL rules, + or %20, and — in Parse mode — the components a browser will really send next to the raw text you pasted, with every parameter decoded and badged: duplicate, bare key, empty value, double-encoded.

It is also the reason not to ask a chatbot. A language model asked to “URL-encode this” is predicting characters: it mixes + and %20 inside one string, forgets that é is two bytes, and confidently “fixes” a double-encoded value by encoding it a third time. This page runs the actual algorithm, byte for byte, and pins every rule it applies with a test vector — so you can check the answer you were given against the spec instead of against a confident paragraph.

Working on a token rather than a URL? The JWT Decoder splits and base64url-decodes a JWT, and the Base64 Encoder / Decoder handles the base64url alphabet.

The Pipeline

How it works.

Four deterministic steps, all inside your browser tab — the raw text is diagnosed before the URL parser is allowed to normalize anything away.

  1. Split the raw text first.

    The input is cut on its own ? and # before any parser touches it, so a diagnostic can point at the exact character — the URL standard strips tabs and newlines silently.

  2. Decode each part alone.

    Names and values are decoded separately, so a %26 inside a value can never turn into a separator and a %3D can never split a name.

  3. Read the bytes as UTF-8.

    Escapes become bytes, bytes become text. A malformed sequence is named byte by byte instead of throwing the URIError decodeURIComponent() would.

  4. Normalize like a browser, show both.

    The WHATWG parser supplies what every HTTP client will actually send — punycode host, dropped default port — with your raw text kept beside it.

Reference

Reserved characters, by component.

RFC 3986 does not have one list of “unsafe” characters. A character is reserved because it means something in a particular position — so whether it needs escaping depends entirely on where it sits.

Character In a path segment In a query name / value
/ gen-delim Must encode (%2F) — otherwise it splits the segment Safe as data, though some proxies normalize it
? gen-delim Must encode (%3F) — it starts the query Safe after the first ?
# gen-delim Must encode (%23) Must encode (%23) — it starts the fragment
[ ] gen-delim Must encode (%5B %5D) Must encode, even in a tags[] style name
: @ gen-delim Safe after the first segment Safe
& sub-delim Safe as data Must encode (%26) — it separates parameters
= sub-delim Safe as data Must encode (%3D) in a name; safe in a value
+ sub-delim Safe as data Must encode (%2B) — bare, it reads as a space
; sub-delim Safe as data Safe today, but a legacy backend may split on it
! $ ' ( ) * , sub-delim Safe as data Safe as data — but encodeURIComponent() disagrees on ! ' ( ) *
% escape marker Always %25 Always %25 — a bare % is what double-encoding is made of
space not allowed %20 %20, or + in a form body

What each mode leaves alone

The safe set is the whole difference between the modes — and between RFC 3986 and the browser built-in.

safe-sets
mode                       left alone (everything else → %XX)

RFC 3986 component         A-Z a-z 0-9 - . _ ~
RFC 3986 whole URL         …plus  : / ? # [ ] @ ! $ & ' ( ) * + , ; =
form-urlencoded            A-Z a-z 0-9 * - . _        (space → +)
encodeURIComponent()       A-Z a-z 0-9 - . _ ~ ! ' ( ) *
                                                 ^^^^^^^^^^
                                       RFC 2396 leftovers — encoded here

Encoded once vs twice

A double-encoded value is not corrupt — it is correct, one layer too deep.

double-encoding
once   https%3A%2F%2Fapp.example.com%2Fcb   →  https://app.example.com/cb
twice  https%253A%252F%252Fapp.example.com  →  https%3A%2F%2Fapp.example.com
                 ^^^                            still escaped: decode again

space  %20    →  " "
       %2520  →  "%20"  →  " "        (the % became %25)

Next Step

The parameter you just decoded is a token. Read it.

Query strings carry access tokens, signed URLs and session ids. Once a value is decoded, the JWT Decoder will split and verify a base64url token, and the Base64 Encoder / Decoder handles anything that is just encoded rather than signed — both, like this page, entirely in your browser.

callback.txt
?state=abc%3D%3D          →  state    = abc==
&channel=%23alerts       →  channel  = #alerts
&next=%252Fsettings      →  next     = %2Fsettings   ← decode again
&utm_source=a&utm_source=b  →  two rows, no winner

FAQ

Questions, answered.

Tap a question to expand the answer.

encodeURIComponent() escapes almost everything, so it is what you use on one piece of a URL — a single query value, a path segment. encodeURI() leaves the reserved characters : / ? # & = + and even # alone, so it is what you use on a URL that is already assembled and only needs its spaces and non-ASCII characters cleaned up. Pass a whole URL to encodeURIComponent and it becomes one opaque blob (https%3A%2F%2F…); pass a single value to encodeURI and any & inside it silently becomes a parameter separator. The Encode mode here exposes both: the default is the component rule, and the "Whole URL" checkbox is byte-for-byte encodeURI().

Both are correct, in different places. RFC 3986 only knows %20. The + convention comes from HTML form submission (application/x-www-form-urlencoded) and applies to form bodies and, by long habit, to query strings — which is why URLSearchParams decodes + as a space but leaves it alone inside a path. That means %20 is always safe, while + is only a space if whoever produced the value said so. The Decode mode shows you which convention it applied and lets you flip it, so you can tell "Ada+Lovelace" the name from "Ada Lovelace" the two words.

Percent-encoding replaces one byte with a % followed by that byte written as two hexadecimal digits. A URL may only carry a small set of ASCII characters, so anything else — a space, an accent, an emoji, or a reserved character being used as data — is first turned into UTF-8 bytes and then each byte is written as %XX. é is two bytes, so it becomes %C3%A9. Decoding reverses it: collect the bytes, then read them back as UTF-8.

Decode it twice. %2520 means the % of %20 was itself encoded to %25, which happens when a client calls encodeURIComponent() on a value that was already encoded — the classic OAuth redirect_uri bug. This tool detects it: paste the value into Decode mode and it reports that the first pass still contains escapes, shows you what a second pass produces, and offers a "Decode again" button. The real fix is upstream: encode exactly once, at the moment you place the value into the URL.

Because the escaped bytes are not valid UTF-8. %C3%A9 is a well-formed two-byte sequence and decodes to é, but %E9 on its own is the Latin-1 (ISO-8859-1) byte for é and is not legal UTF-8 — so it decodes to U+FFFD, the replacement character. This tool names the offending bytes instead of throwing a URIError the way decodeURIComponent() does, which tells you the value was encoded from the wrong source encoding rather than leaving you guessing.

Nothing in the URL standard says which one wins, so every row is listed here and the later ones are badged as duplicates. In practice PHP and Express keep the last value, Go’s r.URL.Query() returns all of them, ASP.NET joins them with commas, and Rails only groups them when the key ends in []. That disagreement is exactly why a tool should show you all the values rather than quietly picking one.

Punycode is the ASCII encoding of an internationalized domain name. DNS only carries ASCII labels, so münchen.example is converted to xn--mnchen-3ya.example before any lookup happens — and that ASCII form is also what a TLS certificate has to match. The parser here shows the converted host with the name you typed underneath it, so you can spot a lookalike domain. It deliberately does not convert punycode back to Unicode: rendering an attacker-supplied name as pretty Unicode is how homograph phishing works.

The unreserved set from RFC 3986 §2.3: A–Z, a–z, 0–9 and the four marks - . _ ~. Those are safe everywhere and must never be encoded gratuitously, because %2D and - are the same URL and re-encoding them breaks signature schemes that hash the raw string. Note that encodeURIComponent() also leaves ! ' ( ) * alone — a leftover from the older RFC 2396 "mark" set. RFC 3986 lists those five as sub-delimiters, so the component mode here escapes them and tells you when it disagreed with the browser built-in.

Not any more. HTML once suggested that servers accept ; alongside & as a parameter separator, and that advice was dropped from the spec in 2014. Browsers and URLSearchParams have never treated it as a separator, so a=1;b=2 is a single parameter named a whose value is the literal 1;b=2. This tool matches that behaviour and warns you when it sees a semicolon, because a legacy backend may still split on it and disagree with the browser sitting in front of it.

Yes — the entire tool is a static page with no server behind it. Parsing, encoding and decoding all run in your browser tab, nothing is uploaded, there is no logging and there is no account. That matters here more than for most tools, because query strings are exactly where access tokens, signed URLs, session ids and email addresses end up. If you want a second opinion, open your browser devtools network panel while you type: there is no request to make.

More free, private DevOps tools.

The URL Encoder / Decoder is one tool in OpsCanopy — a growing canopy of browser-based validators, converters and testers that never touch a server.

39 free tools, every one offline-capable — opscanopy.com works with no signup and nothing uploaded.

Related encoding tools: the Base64 Encoder / Decoder, the JWT Decoder and Slugify for turning a title into a clean path, or browse the full tools directory.

Provided as-is for convenience; percent-encoding is an encoding, not protection — a token in a query string is still a token in a log file. OpsCanopy is free and open.