Base64 Encode / Decode
Encode and decode Base64 and base64url, including files — without mangling non-ASCII text.
the decoded result appears here
Encode and decode, without the mojibake
Base64 turns bytes into ASCII so they survive text-only pipes: an email body, an
HTTP header, a URL segment, a config file. The mechanics are simple — three
bytes become four characters — but the tools most pages bolt on are not. The
native JavaScript pair, btoa and atob, is built on
Latin-1, and Latin-1 is where non-ASCII text goes to be mangled. Encode
café with btoa and it returns
Y2Fm6Q== — valid base64, wrong bytes. Encode
日本語 and it throws outright. This tool encodes the
UTF-8 bytes of your text instead, so what comes out is what a validator in any
language agrees your text actually is. The same correctness runs the other way:
decode to bytes, then decide what those bytes are, rather than printing placeholder
characters into the middle of the result.
Round-tripping files is built in: drop a file and it encodes to a
data:-ready string for you; drop a file containing base64 and the
decoded bytes come back as a downloadable file. A pasted data: URI is
unwrapped automatically, as is a Basic prefix from an
Authorization header — and the notes strip under the input
reports exactly what it removed, so the tool never edits your input silently.
Decode it without this tool
Same job, your tools. The first snippet carries the one caveat that costs people a real afternoon.
printf 'café' | base64 # Y2Fmw6k= printf '%s' 'Y2Fmw6k=' | base64 -d # café # macOS ships BSD base64: it accepts UNPADDED input and silently # truncates the final partial group (exit 0, no warning). printf '%s' 'Y2Fmw6k' | base64 -d # "caf" — wrong, and quiet about it # GNU coreutils rejects the same input instead. Only BSD was # measurable on the machine this was written on. # For one unbroken line GNU uses -w0; macOS's -b does not do the same.
import base64
base64.b64decode("Y2Fmw6k=").decode("utf-8") # "café"
base64.b64decode(seg, validate=True) # rejects non-alphabet chars
base64.urlsafe_b64decode(jwt_seg + "=" * (-len(jwt_seg) % 4))
# validate defaults to False: garbage in, garbage out, silently.
# Pad the final group yourself before decoding URL-safe segments.
Buffer.from('Y2Fmw6k=', 'base64').toString('utf8'); // "café"
Buffer.from(jwtSegment, 'base64url'); // Node ≥ 16 handles -_ and no padding
Buffer.from(data).toString('base64url');
// correct: UTF-8 in, UTF-8 out
new TextDecoder().decode(Uint8Array.fromBase64('w6k=')); // "é"
// (Uint8Array.fromBase64 ships in Chrome 148+; older browsers need a tiny fallback)
// wrong: atob decodes to Latin-1, so a UTF-8 payload comes out mojibake'd
atob('w6k='); // "é"
How base64 is put together
RFC 4648 is the spec this page implements. Section 4 defines the standard alphabet
(A–Z a–z 0–9 + /); section 5 the URL-safe alphabet
that swaps the last two characters for - and _. Padding is
section 3.2: the trailing = marks a final group that has fewer than
three bytes, and a canonical encoder emits at most two of them. Non-alphabet
characters are section 3.3, and whitespace is skipped wherever it appears because
PEM wraps at 64 characters and MIME at 76, and terminal copy-paste adds its own.
Section 3.5 is the subtle one: the leftover bits in a partial final group must be
zero, and this page reports a string that violates that instead of failing on it
— decode QR== and it notes “non-canonical tail”, because the
encoder that produced it was not conforming. Every one of those decisions is
reported in the notes strip under the input, so the tool shows its working about
your input rather than swallowing it.
FAQ
Why does btoa("café") give the wrong answer, and btoa("日本語") throw?
btoa takes a string of code points 0–255 and writes each one into
a single byte. é is U+00E9, which fits in one byte, so
btoa("café") encodes the Latin-1 byte 0xE9
and produces 6Q== — the bytes the UTF-8 form of
café does not contain. U+0080–U+00FF, the Latin-1
supplement — é ü ñ ö å ç à ß
— encodes silently wrong: no warning, valid base64, wrong bytes.
U+0100 and above throws. The fix is to encode the string to UTF-8 bytes first and
encode those bytes, which is what this page does. It is checkable in a console, and
it is the measurement this page was built around.
What is base64url, and when do I need it?
RFC 4648 §5 swaps + and / for - and
_, because those two characters are not URL-safe and collide with other
syntax. JWTs, signed URLs and filenames all use it, and it usually omits the
trailing = padding because padding is redundant and URL parsers dislike
it. The alphabets differ in two characters, which is why a JWT decoded with the
standard alphabet quietly produces wrong bytes from the signature on — and why
this page reports which alphabet it detected instead of guessing.
Is base64 encryption?
No. Base64 is a transfer encoding: it turns bytes into ASCII so they
survive a text-only medium. Anyone can decode it, including every server it passes
through. The everyday example is HTTP Basic auth: the credentials after
Authorization: Basic decode to
user:password in plaintext. This page strips that prefix for you and
decodes the rest, which is convenient and doubles as a demonstration of why Basic
auth is not security. Nothing you paste is uploaded — the browser is
instructed to block every request this page could make, and you can read the policy
off the response header yourself: here is how to check it.
Why did my base64 decode produce é instead of é?
The base64 string is fine; whoever decoded it treated its bytes as Latin-1 and
printed one character per byte. The UTF-8 bytes of é are
C3 A9, and read as Latin-1 those are à and
©. The fix is to decode to bytes and then decode the bytes as
UTF-8: new TextDecoder().decode(Uint8Array.fromBase64("w6k=")).
This page runs that text check automatically, and where the bytes are not valid
UTF-8 at all it says so instead of printing replacement characters. If the paste
instead came out of a CMS export — full of ’ and
é rather than base64 — that is HTML entity encoding,
and its own page decodes the legacy
Windows-1252 range too.
Why is the padding missing from my JWT, and why is that correct?
JWT segments are base64url without padding (RFC 7515 §3). A fully padded
encoder writes = to round a final group up to a multiple of four; the
JWT formats drop it because nothing downstream needs it. The trap is a tool that
does not understand unpadded input and guesses: BSD base64 -d, which
macOS ships, accepts it and silently drops the final partial group instead of
failing — measured here, a payload ending {"exp":1516242622}
comes back as {"exp":151624262. If a decode looks plausible but
wrong by a factor of ten, that is what happened. The
jwt-debugger page has the details and the padded one-liner that fixes it.
The decoded output is binary. What now?
Then the base64 was the encoding of a file, not text. Printing it as text would show
replacement characters that tell you nothing. This page sniffs the decoded bytes
against the magic numbers of common formats, reports the byte count and what it
looks like, and offers the file for download — a PNG decodes to
image.png, a ZIP to archive.zip. Decoding back into a file
is the other half of the file workflow here, and it never runs a UTF-8 decode it
does not need: bytes that are not text stay bytes.