Skip to main content
Early Bird: early members keep launch perks Premium planned, not on sale yet.Sign Up Free →
Developer workflows9 min read

Download a PDF from Base64 or Uint8Array in JavaScript

An API hands you a long string that starts with JVBERi0. You do not need to generate a PDF — you need to restore the bytes, name the file, and prove you received a document.

By BytesPDF Editorial TeamPublished
Base64 characters decoding into a browser-local PDF download

An API response lands in your console. The field is too long to read, begins with JVBERi0, and someone says, “download this PDF.” The easy mistake is to treat that string as text and save it directly. The browser then gives you a file named invoice.pdf that a reader cannot open.

The hard part is usually not making a PDF. The PDF already exists. Your job is to restore its original bytes, hand those bytes to the browser as a file, and check that the value was not clipped, escaped, or replaced by an error response along the way.

This guide is for a browser-side workflow. It does not turn arbitrary JSON, text, an image, or a random byte array into new PDF pages. It restores a file only when the input already contains a complete PDF.

First: identify what you actually received

A PDF represented as text generally arrives in one of four forms:

Information about What you have by What it looks like, What to do
What you haveWhat it looks likeWhat to do
Standard Base64JVBERi0xL...Decode it into bytes.
A Base64 data URLdata:application/pdf;base64,JVBERi0...Remove the prefix, then decode the value after the comma.
Base64URLUses - and _ instead of + and /; padding may be omittedNormalize the URL-safe alphabet or use a decoder configured for it.
Existing bytesUint8Array, ArrayBuffer, or a byte-array responseKeep the bytes; do not Base64-decode them again.

JVBERi0 is common because it decodes to %PDF-, the usual PDF header. It is a helpful smell test, not proof. A server can return a Base64-encoded error page just as easily as a document, and a truncated PDF may still have a convincing beginning.

Base64 itself is just a transport spelling for bytes. RFC 4648 defines the ordinary Base64 alphabet and its URL-safe sibling. The URL-safe form swaps +// for -/_; some systems also omit trailing = padding. That difference is why a value copied from a token, URL, or JSON payload may fail a naïve atob() call even when the underlying file is fine.

The browser-safe path: text → bytes → Blob → download

atob() decodes Base64, but its result is a binary string: every character represents one byte. That is not the same as a normal JavaScript text string you should display, concatenate with prose, or UTF-8 encode. Copy those character codes into a Uint8Array, then make a Blob with the PDF MIME type.

Here is a compatibility-first example for modern browsers. It accepts a raw Base64 value, a Base64 data URL, wrapped lines, and the URL-safe Base64 alphabet.

function decodeBase64Pdf(input) {
  let encoded = input.trim();
  if (!encoded) throw new Error("No Base64 value was supplied.");

  // A data URL has metadata before its first comma.
  if (/^data:/i.test(encoded)) {
    const comma = encoded.indexOf(",");
    const metadata = encoded.slice(0, comma);
    if (comma < 0 || !/;base64$/i.test(metadata)) {
      throw new Error("Expected a Base64 data URL.");
    }
    encoded = encoded.slice(comma + 1);
  }

  // Line wrapping is common in exported values. Base64URL uses - and _.
  encoded = encoded
    .replace(/\s+/g, "")
    .replace(/-/g, "+")
    .replace(/_/g, "/");

  const unpadded = encoded.replace(/=+$/, "");
  if (unpadded.length % 4 === 1) {
    throw new Error("The Base64 value is incomplete or malformed.");
  }

  const padded = unpadded + "=".repeat((4 - (unpadded.length % 4)) % 4);
  const binary = atob(padded); // Throws if the value is not valid Base64.

  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}

function downloadPdfBytes(bytes, filename = "document.pdf") {
  const blob = new Blob([bytes], { type: "application/pdf" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");

  link.href = url;
  link.download = filename;
  link.hidden = true;
  document.body.append(link);
  link.click();
  link.remove();

  // The click has been dispatched; release the temporary browser reference.
  window.setTimeout(() => URL.revokeObjectURL(url), 0);
}

const bytes = decodeBase64Pdf(apiResponse.pdf);
downloadPdfBytes(bytes, "invoice.pdf");

Blob is a file-like container for immutable raw data. URL.createObjectURL() creates a temporary blob: URL that the download link can use; URL.revokeObjectURL() releases that reference after the download flow has started. If you keep a preview open or save the object URL in application state, revoke it only when that preview is finished.

The MIME type is a label, not validation. new Blob([bytes], { type: "application/pdf" }) does not inspect the bytes, repair a damaged file, strip active content, or make a non-PDF render as a PDF. It simply tells the browser how you intend to handle those bytes.

Prefer direct bytes when the API can return them

Base64 adds size and memory overhead. If you control the API contract, a binary response is usually cleaner than wrapping the document in JSON text.

For example, fetch a same-origin or permitted file endpoint as an ArrayBuffer, then put those bytes directly into a Blob:

const response = await fetch("/documents/statement.pdf");
if (!response.ok) throw new Error(`Download failed: ${response.status}`);

const bytes = new Uint8Array(await response.arrayBuffer());
downloadPdfBytes(bytes, "statement.pdf");

If your client already has a Uint8Array, pass it to downloadPdfBytes() as-is. Do not call atob() on it. If you are debugging an API that emits hexadecimal text or a literal such as Uint8Array([37, 80, 68, 70, ...]), convert that representation to bytes once, then use the same Blob/download step.

Newer browsers also expose Uint8Array.fromBase64(), which returns bytes directly and can be configured for base64url. It is a tidy option for an app whose supported browsers have it. MDN marks it as newly available since 2025, though, so an atob() fallback remains the safer choice for a broad compatibility promise.

Check the envelope before you trust the download

For a quick client-side guard, inspect the first and last part of the byte array:

  • Look for %PDF- near the beginning.
  • Look for %%EOF near the end.
  • Reject an empty response, a suspiciously tiny response, or a result far smaller than the document you expected.
  • Open the download in a current PDF reader and inspect every page, signature, barcode, form value, and attachment that matters.

Those markers catch common failures: a clipped Base64 field, JSON decoded as though it were a PDF, or an API that returned an HTML login/error page. They are not a structural validation. A PDF can include malformed objects, missing content, unsupported features, encryption, or a damaged cross-reference table while still containing both markers.

Treat an untrusted PDF as untrusted after decoding. Saving it locally does not sanitize it. Keep the reader current, follow your organization’s file-handling policy, and never treat a successful download as evidence that a legal, financial, or signed document is intact.

The failures that waste the most time

InvalidCharacterError or a Base64 decoding error. Check whether you copied the data:...;base64, prefix into a decoder that expects only the payload; whether URL-safe -/_ values need normalization; and whether logging, HTML escaping, or line wrapping changed the string. A length remainder of one after padding is removed is not a recoverable Base64 payload—it is incomplete.

The file downloads but the reader says it is damaged. Inspect the HTTP status and response headers before you inspect the PDF code. Authentication redirects and JSON error bodies are common impostors. Then compare the decoded byte count with the server-side original, if you control both sides.

The file opens but is blank or wrong. You may have selected the wrong API field, decoded one layer too many, or received a valid PDF for a different record. A %PDF- header cannot tell you whose document it is.

The page freezes with a large value. Decoding temporarily holds the text representation, a binary string, a typed array, and a Blob in browser memory. Avoid copying huge documents through several intermediate strings. Use a binary response or a streaming design when you control the system; do not promise that any browser can safely process an arbitrary payload size.

Use a local tool when the code is not the job

If you have existing PDF bytes but do not need to ship this workflow in an application, use Byte Array to PDF. It runs in the browser, accepts Base64 (including Base64URL and Base64 data URLs), hex/byte-array input, or raw byte strings, and creates a download from the unchanged bytes.

The utility checks for %PDF- near the start and %%EOF near the end. That is deliberately a quick guard, not a parser, repair service, or certification step. Its decoded-input limit is 20 MB, so a larger file needs a workflow designed for the memory available on the user’s device.

If the restored PDF then needs to fit an email or portal cap, that is a separate decision: keep the recovered file as the source, make a copy, and compress the copy locally only if the document can tolerate image recompression. For attachment math and recipient limits, use the email and messaging workflow. Do not confuse Base64 decoding with compression; decoding restores the original bytes, while compression rewrites a PDF and can change it.

A practical handoff checklist

  • Confirm the payload represents a PDF already; do not fabricate pages from arbitrary bytes.
  • Record whether the source is standard Base64, Base64URL, a data URL, or direct bytes.
  • Decode once into a Uint8Array; do not turn the binary result into normal text.
  • Create a Blob with application/pdf, trigger the download, and release the object URL when it is no longer needed.
  • Check the header/trailer as a quick guard, then open the result in a real reader.
  • Keep the original response and compare the actual document contents before you send, file, or sign it.

The useful mental model is simple: Base64 is packaging, not a document format. Decode it carefully, preserve the bytes you received, and verify the PDF you hand to a person or a workflow.

Sources and further reading

Frequently asked questions

Can JavaScript turn any Base64 string into a PDF?

No. JavaScript can decode Base64 into bytes and label a download as application/pdf, but the source bytes must already be a complete PDF. A Base64 string containing JSON, an image, an HTML error page, or truncated data does not become a valid PDF merely because the filename ends in .pdf.

Why does a Base64 PDF often start with JVBERi0?

That is the Base64 representation of the PDF header beginning %PDF-. It is a useful quick clue, not a full validity test: a complete file also needs intact internal objects and a usable trailer.

Should I use atob() or Uint8Array.fromBase64()?

For a compatibility-first browser path, atob() plus a Uint8Array conversion remains useful. Modern browsers also offer Uint8Array.fromBase64(), which returns bytes directly, but MDN marks it as newly available since 2025, so test the devices you support before making it your only path.

Does creating a Blob validate or repair the PDF?

No. Blob only packages bytes with a MIME-type hint; it does not parse, validate, repair, sanitize, decrypt, or render them. Open the downloaded file in a current PDF reader and verify the pages and fields that matter.

Can BytesPDF convert an arbitrary byte array, image, or text into new PDF pages?

No. BytesPDF's Byte Array to PDF utility saves bytes that already represent a PDF. It checks for a PDF header and end marker, but it does not create PDF pages from arbitrary source data or provide a public conversion API.