SendGrid: Send an Inline Image with Base64 (and Why a Data URI Gets Stripped)
You have a logo or a chart that must appear inside the email body via SendGrid — no paperclip, no hosted URL. The official docs explain the concept; this page is the complete working payload: Base64 in the attachments array with content_id and disposition: "inline", a cid: reference in the HTML, in Node.js, Python, and curl — plus the four reasons the image still arrives blank.
The working pattern
Three moving parts, all required:
- The Base64 string — raw characters only. No
data:image/png;base64,prefix, no line breaks. The MIME type goes in the separatetypefield. - An attachments entry with
disposition: "inline"and acontent_idyou choose. - A
cid:reference in the HTML body that matches thecontent_idexactly (case-sensitive).
attachments: [
{
"content": "<raw base64, no prefix>",
"filename": "logo.png",
"type": "image/png",
"disposition": "inline",
"content_id": "logo001"
}
]
html: '<img src="cid:logo001" width="200">'
SendGrid reassembles your JSON into a MIME message on send, and cid: references resolve inside that MIME structure — the one mechanism Gmail, Apple Mail, Outlook desktop, and Outlook web all render. Getting the Base64 string right is step zero: encode in the browser with the image to Base64 converter or in code as shown below, and see the image to Base64 for API guide for the encoding patterns behind step 1.
Node.js example
const sgMail = require('@sendgrid/mail');
const fs = require('fs');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const base64 = fs.readFileSync('logo.png').toString('base64');
const msg = {
to: 'user@example.com',
from: 'sender@example.com',
subject: 'Weekly report',
html: '<img src="cid:logo001" width="200">',
attachments: [
{
content: base64, // raw Base64 — no data: prefix, no line breaks
filename: 'logo.png',
type: 'image/png',
disposition: 'inline',
content_id: 'logo001' // must match cid:logo001 exactly
}
]
};
sgMail.send(msg);
Python example
import base64, os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import (
Mail, Attachment, FileContent, FileType,
FileName, Disposition, ContentId
)
with open('logo.png', 'rb') as f:
b64 = base64.b64encode(f.read()).decode()
msg = Mail(
from_email='sender@example.com',
to_emails='user@example.com',
subject='Weekly report',
html_content='<img src="cid:logo001" width="200">'
)
msg.attachment = Attachment(
file_content=FileContent(b64),
file_type=FileType('image/png'),
file_name=FileName('logo.png'),
disposition=Disposition('inline'),
content_id=ContentId('logo001')
)
SendGridAPIClient(os.environ['SENDGRID_API_KEY']).send(msg)
curl example
curl -X POST https://api.sendgrid.com/v3/mail/send \
-H "Authorization: Bearer $SENDGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"personalizations": [{"to": [{"email": "user@example.com"}]}],
"from": {"email": "sender@example.com"},
"subject": "Weekly report",
"content": [{"type": "text/html",
"value": "<img src=\"cid:logo001\" width=\"200\">"}],
"attachments": [{
"content": "iVBORw0KGgoAAAANSUhEUg...",
"filename": "logo.png",
"type": "image/png",
"disposition": "inline",
"content_id": "logo001"
}]
}'
If the Base64 was produced by a command-line pipeline, generate it without wrapping — base64 -w 0 logo.png on Linux — or the newlines will break the JSON payload itself. The full command-line pattern set is in the API encoding guide.
Why the data URI fails
It is tempting to skip attachments and paste <img src="data:image/png;base64,..."> straight into the html field. SendGrid will accept it and the message will send — but Gmail strips data URIs during sanitization, Outlook desktop never renders them (its Word engine has no data URI support), and the recipient sees a blank box. Data URIs are a web-page mechanism; email clients treat them as untrusted by default.
| Method | Gmail | Outlook desktop | Attachment icon |
|---|---|---|---|
| attachments + content_id (CID) | Renders | Renders | None |
| data: URI in HTML | Stripped | Never renders | None |
| disposition "attachment" | Renders (as file) | Renders (as file) | Shows paperclip |
The same restriction drives the platform-specific guides: Gmail signature Base64 not working, Base64 image not showing in Outlook, and the low-code equivalents Power Automate / Apps Script — in every case the fix is CID, not a better data URI.
Image not showing? The 4 causes
- content_id / cid: mismatch. The reference is case-sensitive and must match character for character —
cid:Logo001does not resolve an attachment id oflogo001. The only symptom is a silent blank box. - Missing
disposition: "inline". Without it the attachment defaults to a regular file: the image arrives as a paperclip instead of rendering in the body. - The
data:prefix left incontent. The field must be raw Base64; a prefix makes SendGrid decode corrupt bytes, and the client shows a broken image. Same family: line breaks inside the Base64 (see curl section). - A data URI used instead of CID. If the HTML contains
src="data:...", Gmail strips it and Outlook desktop ignores it — the send succeeded, the mechanism didn't. Switch to the attachments pattern above.
Corrupted-looking output after decode is a broader topic — the fix invalid Base64 image data guide covers prefix duplication, whitespace, and truncation issues.
Frequently asked questions
How do I embed an inline image with SendGrid?
Base64-encode the image, add it to attachments with disposition: "inline" and a content_id, and reference <img src="cid:your-content-id"> in the HTML body.
Why doesn't a data URI work in SendGrid emails?
Gmail strips data URIs during sanitization and Outlook desktop never renders them. The message sends, the image doesn't show. Use the CID attachment pattern.
Should the content field include the data: prefix?
No — raw Base64 only. The prefix corrupts the decoded attachment; the MIME type belongs in the type field.
Why is my inline image not showing?
Check in order: cid/content_id match (case-sensitive), disposition "inline" present, no data: prefix or line breaks in content, and no data URI in the HTML.
How do I get the Base64 string?
fs.readFileSync(file).toString('base64') in Node, base64.b64encode(f.read()).decode() in Python, or a browser converter — see the API guide.
Do inline images work in Gmail and Outlook?
Yes, with the CID pattern — it is standard MIME and renders everywhere, unlike data URIs which Gmail strips and Outlook desktop ignores.
Need the Base64 string? Encode it in the browser with the Image to Base64 converter, size it with the Base64 size calculator, and paste the raw output straight into the content field.