Base64 vs Base64url: the difference, and why your JWT would not decode.

Two characters and a padding rule. That is the entire difference, and it is enough to make a decoder throw "invalid character" at a perfectly good token.

Published 2026-09-15 · 4 min read · Kirk Diamond

What Base64 is for

Base64 turns any sequence of bytes into text using 64 printable ASCII characters, three bytes at a time into four characters. It exists so binary data can travel through systems designed for text: email bodies, JSON strings, HTTP headers, XML. It is not encryption and it is not compression; the output is a third larger than the input and anyone can reverse it.

The two alphabets

Standard Base64 (RFC 4648 §4)Base64url (RFC 4648 §5)
Characters 0 to 61A-Z a-z 0-9A-Z a-z 0-9
Character 62+-
Character 63/_
Padding= to a multiple of 4Usually omitted
Used byMIME email, Authorization: Basic, data URIs, PEM certificates, most APIs and config filesJWT, WebAuthn, OAuth PKCE, most things that put a value in a URL or a cookie

The reason for the second alphabet is in the name. +, / and = all have meanings in a URL: + is a space in query strings, / separates path segments, = separates keys from values. A standard Base64 value dropped into a URL either gets mangled or has to be percent-encoded (%2B%2F%3D), which is ugly and error-prone. Base64url swaps in - and _, which are safe everywhere, and drops the padding because the length tells the decoder how many bytes to expect anyway.

What goes wrong

Decoding Base64url with a standard decoder

The decoder meets - or _, which are not in its alphabet, and fails: Invalid character, illegal base64 data at input byte N, Incorrect padding. This is the classic JWT decoding error. Every segment of a JWT is Base64url without padding, and atob() in a browser, base64.b64decode in Python and base64.StdEncoding in Go are all standard decoders. The JWT decoder handles this for you; if you are decoding by hand, use the Base64 encoder/decoder with the scheme set to Base64url.

Padding

Standard Base64 output is always a multiple of four characters, padded with one or two =. Base64url usually has none. Strict standard decoders reject unpadded input (Incorrect padding); some lenient ones accept it. Strict Base64url decoders reject padded input. To convert, add = until the length is a multiple of four, or strip them.

Standard Base64 in a URL

A + in a query parameter arrives at the server as a space. A / may be interpreted as a path separator. The decoder sees a space or a truncated string and fails, but only for values that happened to contain a 62 or 63, so it works most of the time and fails intermittently. If a value ever goes near a URL, use Base64url or percent-encode it.

Line wrapping

MIME Base64 (RFC 2045) inserts a line break every 76 characters, and openssl base64 does the same every 64. Decoders that do not expect newlines fail. Strip all whitespace before decoding; the codec on this site does so automatically.

Converting between them

# standard to url-safe
tr '+/' '-_' <<< "$B64" | tr -d '='

# url-safe to standard (then pad)
s=$(tr '-_' '+/' <<< "$B64URL"); while (( ${#s} % 4 )); do s+="="; done; echo "$s"
LanguageBase64url decode
JavaScriptatob(s.replace(/-/g,'+').replace(/_/g,'/').padEnd(Math.ceil(s.length/4)*4,'='))
Pythonbase64.urlsafe_b64decode(s + '=' * (-len(s) % 4))
Gobase64.RawURLEncoding.DecodeString(s)
JavaBase64.getUrlDecoder().decode(s)
PHPbase64_decode(strtr($s, '-_', '+/'))
RubyBase64.urlsafe_decode64(s)

Go's Raw encodings are the ones without padding; URLEncoding alone still expects =. Python's urlsafe_b64decode handles the alphabet but still wants padding, hence the arithmetic.

Which to use

If the value will ever appear in a URL, a cookie, a filename or a JWT: Base64url, no padding. If it is going into an email, a PEM file, a Basic auth header or an API that documents "Base64": standard, with padding. If you are decoding something someone else produced, look at the characters: - or _ means url-safe, + or / means standard, and trailing = means padded. Neither one is more secure than the other, because neither one is secure at all; they are encodings, and anything encoded can be decoded by anyone.

All guides · All tools