Google Apps Script: Send an Email with a Base64 Image (the inlineImages Pattern)
You have a Base64 image — an API response, a spreadsheet-driven report, a generated chart — and you want it inside the email your script sends. The instinctive move is to paste a data:image/png;base64, URI into htmlBody, and it fails silently: Gmail does not render data URIs in email bodies. The supported route is different: decode the Base64 into a blob and attach it as an inline image with a CID reference. Below is the exact working pattern, the prefix-stripping step people miss, and the four reasons the image still shows up blank.
The working pattern
Three steps, all in one script:
function sendEmailWithImage() {
const base64Data = "iVBORw0KGgo..."; // raw Base64, no data: prefix
const blob = Utilities.newBlob(
Utilities.base64Decode(base64Data),
"image/png", // must match the real format
"logo.png"
);
MailApp.sendEmail({
to: "recipient@example.com",
subject: "Weekly report",
htmlBody: '<p>Hi — here is this week\'s chart.</p>' +
'<img src="cid:reportChart" width="480" alt="Chart">',
inlineImages: { reportChart: blob }
});
}
What each piece does:
Utilities.base64Decode()turns the string back into raw bytes.Utilities.newBlob()wraps the bytes as a file-like blob — the contentType must match the real format (image/pngvsimage/jpeg), because the client trusts it when rendering.inlineImagesis a map of CID keys to blobs. Each key becomes an attachment-like inline part, referenced from the HTML withsrc="cid:key"— the same mechanism email clients have used for embedded images for decades.
Because the image travels as a proper MIME part, it renders in Gmail, Apple Mail, Thunderbird and Outlook desktop alike — no data URI restrictions apply.
If your Base64 has the data: prefix
Base64 coming from a converter tool or an API often arrives wrapped: data:image/png;base64,iVBORw0KGgo.... Utilities.base64Decode wants only the payload — a full data URI makes it throw an invalid argument error:
const payload = fullDataUri.split(",")[1]; // strips "data:image/png;base64,"
const blob = Utilities.newBlob(Utilities.base64Decode(payload), "image/png", "logo.png");
The prefix is not wasted information — the MIME type it declared belongs in the blob's contentType. Check the two agree: declaring image/png over JPEG bytes renders as broken in most clients. If you generated the string yourself, the Image to Base64 converter outputs the MIME type alongside the data so you can wire both correctly.
One more transport gotcha: if the Base64 was produced by a command line, it may contain line breaks every 76 characters. Strip them before decoding — payload.replace(/\s/g, "") — or the decode silently corrupts. The command-line side of this is covered in the image to Base64 for API guide.
If the image is already a file or URL
Base64 decoding is only necessary when your source is a Base64 string. Apps Script has richer image sources, and inlineImages accepts any blob:
- Drive file:
DriveApp.getFileById(id).getBlob()— no encoding step at all. - Hosted image:
UrlFetchApp.fetch(url).getBlob()— fetch and forward in one line. - HTTP response payload: if you already fetched bytes,
response.getBlob()is the blob.
Reaching for Base64 when you hold a blob is added work and added failure modes (wrong MIME, prefix confusion) — pass the blob directly.
Four ways to get an image into the email
| Method | No attachment icon | Renders everywhere? | Best for |
|---|---|---|---|
| inlineImages + cid: | Yes | Yes | Logos and charts in app notifications — the pattern on this page |
| data URI in htmlBody | Yes | No — Gmail strips it | Nothing; listed so you don't debug it twice |
| Hosted HTTPS URL | Yes | Yes | When the image already lives at a stable URL |
| attachments: [blob] | No | Yes | Files the recipient should keep (invoices, exports) |
The data URI row is the trap this page exists for: it works in browsers, which is why converter tools show you one, and it dies in every mail client. That client-side restriction is not Apps Script specific — the client-by-client breakdown is in the Base64 image not showing in Outlook guide, and Power Automate runs into the same wall. The difference: Apps Script mail goes through Gmail's own send pipeline, so the CID pattern above is first-class here.
Image not showing? The 4 causes
- The data URI never left your script. You put
<img src="data:image/png;base64,...">inhtmlBody— Gmail strips it on send or render. There is no flag to enable it; switch to theinlineImagespattern. - CID key mismatch. The key in
inlineImagesand the string insrc="cid:..."must match exactly, including case.{ image1: blob }pairs withcid:image1, notcid:logo.png. - Wrong or missing MIME type.
Utilities.newBlob(bytes)without a contentType producesapplication/octet-stream, which some clients refuse to render inline. Always pass the real format as the second argument. - The payload is corrupted. A data-URI prefix left in the string throws on decode; embedded line breaks corrupt it silently. Strip the prefix with
split(",")[1]and whitespace with.replace(/\s/g, ""). If the decoded blob's size looks wrong next to the source file, this is why.
If the image is destined for a signature typed into Gmail's settings rather than a script-sent mail, the mechanics differ per client — start from the Base64 image in email signature guide, or the Gmail signature troubleshooting page if it already went wrong.
Keep the payload small
Inline images ride inside the message, and Base64 adds about 33% overhead on top of the file size — a 300 KB chart becomes a 400 KB email before any text. Large payloads also burn through MailApp daily quota faster. Before encoding:
- Shrink the file: compress PNG or compress JPG first — logos and charts want to live in the tens of KB.
- Predict the encoded weight with the Base64 size calculator before it lands in a mailbox.
- Encode in the browser with the image to Base64 converter — nothing is uploaded — and copy the MIME type and payload separately for the
newBlobcall.
Frequently asked questions
Why doesn't a Base64 data URI image show in my Apps Script email?
Gmail does not render data: URIs in email bodies. Decode the string to a blob (Utilities.newBlob(Utilities.base64Decode(str), 'image/png', 'logo.png')) and send it via inlineImages, referenced from the HTML as src="cid:key".
How do I convert a Base64 string to an inline image in Apps Script?
const blob = Utilities.newBlob(Utilities.base64Decode(str), "image/png", "logo.png"), then inlineImages: { image1: blob } in the send options and <img src="cid:image1"> in htmlBody.
My Base64 starts with data:image/png;base64, — do I keep the prefix?
No — str.split(",")[1] keeps only the payload the decoder expects; the MIME goes into the blob's contentType. A full data URI passed to base64Decode throws an invalid argument error.
Does the inlineImages pattern work with GmailApp too?
Yes — GmailApp.sendEmail takes the same inlineImages map in its advanced options, alongside htmlBody. MailApp works identically but doesn't record the message in the sent view.
Is there a size limit for inline images?
The message must stay under Gmail's ~25 MB send limit, but practical trouble starts earlier: large inline images bloat the mail and burn MailApp quota. Keep sources in the tens of KB — Base64 adds ~33% on top.
Can I skip Base64 if the image is already a file or URL?
Yes — DriveApp.getFileById(id).getBlob() or UrlFetchApp.fetch(url).getBlob() feed inlineImages directly. Decode only when your source is already a Base64 string.
Generating the Base64 by hand? Encode it in the browser with the Image to Base64 converter, check the final weight with the size calculator, and split the output into MIME type + payload for your newBlob call.