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. Encodecafé with btoa and it returnsY2Fm6Q== — 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 adata:-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 anAuthorization 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, sobtoa("café") encodes the Latin-1 byte 0xE9and produces 6Q== — the bytes the UTF-8 form ofcafé 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 afterAuthorization: Basic decode touser: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 é areC3 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.
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 toimage.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.