← back to blog
web 7 min read May 2026

URL Encoding Explained: %20, +, and the Differences That Break APIs

A space can be represented as %20 or as + in a URL. These two representations look interchangeable but they're not. Using the wrong one in a query parameter, a form POST body, or an OAuth signature string is a subtle bug that silently corrupts data, breaks authentication, or causes cryptic 400 errors that are frustrating to diagnose.

Why URLs need encoding

URLs are restricted to a subset of ASCII characters. Many characters — spaces, special symbols, non-ASCII letters — cannot appear literally in a URL because they'd be ambiguous or break the URL's syntax. A ? marks the start of a query string. A & separates query parameters. A # starts a fragment. If these characters appear in your data, they must be escaped.

Percent-encoding (also called URL encoding) is the solution: replace the problematic character with a percent sign followed by its two-digit hexadecimal ASCII code. A space (ASCII 32, hex 20) becomes %20. A & (ASCII 38, hex 26) becomes %26. An @ (ASCII 64, hex 40) becomes %40.

Reserved vs unreserved characters

The URL specification (RFC 3986) divides characters into two categories:

Unreserved characters — safe to use anywhere in a URL without encoding:

A-Z a-z 0-9 - _ . ~

Reserved characters — have special meaning in URL syntax and must be percent-encoded when they appear as data (not as their structural meaning):

: / ? # [ ] @ ! $ & ' ( ) * + , ; =

Everything else — spaces, non-ASCII characters, ", <, >, |, and so on — must always be percent-encoded.

%20 vs +: the critical difference

Both %20 and + represent a space — but they're defined in different contexts and are not interchangeable.

%20 is the standard percent-encoding of a space character, defined by RFC 3986. It is valid anywhere in a URL where a space needs to be encoded: path segments, query strings, fragments.

+ as a space representation is defined only in the application/x-www-form-urlencoded format — the encoding used when HTML forms submit data via POST. It is specific to query strings in this format. A literal + in a URL that doesn't use form encoding means a plus sign, not a space.

ContextSpace encodingNotes
URL path segment%20Always percent-encode
URL query string (RFC 3986)%20Standard encoding
HTML form POST body+application/x-www-form-urlencoded
HTML form GET (query string)+Browsers use form encoding for forms
OAuth 1.0 signature base string%20Must use percent-encoding, not +
REST API query parameters%20Use encodeURIComponent

A real example of the bug

Your API receives a search query. The user searches for "hello world". You build the URL:

// Wrong — builds URL with literal space (invalid URL) const url = `https://api.example.com/search?q=${query}`; // Also wrong — uses form encoding, may break non-form APIs const url = `https://api.example.com/search?q=${query.replace(' ', '+')}`; // Correct — standard percent-encoding for URL query parameters const url = `https://api.example.com/search?q=${encodeURIComponent(query)}`; // Result: https://api.example.com/search?q=hello%20world

If the server uses a framework that decodes + as a space (many do), both versions might work. But if the server uses strict RFC 3986 decoding, it interprets + as a literal plus sign. Your "hello world" search becomes "hello+world" — a search for a string that probably returns zero results.

encodeURIComponent vs encodeURI

JavaScript provides two encoding functions that are frequently confused:

encodeURIComponent() encodes everything except unreserved characters (A-Z a-z 0-9 - _ . ! ~ * ' ( )). Use this for encoding individual parameter values before adding them to a URL. It encodes ?, &, =, /, and every other reserved character.

encodeURI() encodes everything except unreserved characters AND the reserved characters that have structural meaning in a URL (: / ? # [ ] @ ! $ & ' ( ) * + , ; =). Use this to encode a complete URL that already has its structure in place. It will not encode the ? that starts the query string or the & between parameters.

const value = "price=10&discount=5%"; encodeURIComponent(value) // → "price%3D10%26discount%3D5%25" // Encodes = and & — correct for a parameter value encodeURI("https://example.com/search?" + value) // → "https://example.com/search?price=10&discount=5%25" // Does NOT encode = and & — they're interpreted as URL structure // This is wrong for our value! The = and & break the query string

The rule: always use encodeURIComponent() for parameter values. Only use encodeURI() for encoding a complete, already-structured URL (and even then, it's rarely needed).

Double encoding: a common mistake

Double encoding happens when you encode a value that's already encoded. A space becomes %20, then %20 gets encoded to %2520 (because % encodes to %25).

// Original value "hello world" // Encoded once (correct) "hello%20world" // Encoded twice (wrong — the % gets encoded) "hello%2520world" // Server decodes it: "hello%20world" — a literal percent-twenty, not a space

This commonly happens when building URLs in multiple steps — encoding a parameter value, then encoding the whole query string again, or passing an already-encoded URL through another encoding function.

Watch for this in logging and debugging: If you see %25 where you expected %, you have double encoding. If you see %2520, the original value had a space that was percent-encoded, and then that encoding was encoded again.

Non-ASCII characters

Non-ASCII characters (accented letters, CJK characters, emoji) must be UTF-8 encoded first, then each byte percent-encoded.

"café" → UTF-8 bytes: 63 61 66 C3 A9 → percent-encoded: "caf%C3%A9" "😊" → UTF-8 bytes: F0 9F 98 8A → percent-encoded: "%F0%9F%98%8A"

encodeURIComponent() in JavaScript handles this correctly — it UTF-8 encodes the character before percent-encoding. Don't try to do this manually.

Practical checklist

Use URLSearchParams: In modern JavaScript, new URLSearchParams({q: "hello world", page: 1}).toString() correctly produces q=hello+world&page=1 (form encoding). For REST APIs, use encodeURIComponent on each value and join manually, or use a URL building library.

// summary