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:

  1. The Base64 string — raw characters only. No data:image/png;base64, prefix, no line breaks. The MIME type goes in the separate type field.
  2. An attachments entry with disposition: "inline" and a content_id you choose.
  3. A cid: reference in the HTML body that matches the content_id exactly (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.

Inline data URI vs. CID attachment via SendGrid
MethodGmailOutlook desktopAttachment icon
attachments + content_id (CID)RendersRendersNone
data: URI in HTMLStrippedNever rendersNone
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

  1. content_id / cid: mismatch. The reference is case-sensitive and must match character for character — cid:Logo001 does not resolve an attachment id of logo001. The only symptom is a silent blank box.
  2. Missing disposition: "inline". Without it the attachment defaults to a regular file: the image arrives as a paperclip instead of rendering in the body.
  3. The data: prefix left in content. 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).
  4. 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.