---
title: Host Images for HTML Email Without Base64
description: Upload an email image in a preparation step, then use a public URL with alt text, width, and height.
slug: host-images-html-email
date: 2026-08-20
updated: 2026-08-20
last_tested: 2026-08-20
summary: Upload the image before email delivery, then reference its public URL from an HTML img element instead of embedding base64 data.
cluster: Automation workflows
intent: workflow
sources:
  - title: MDN HTML img element
    url: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img
  - title: Apple Mail Privacy Protection
    url: https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios
  - title: Gmail external image settings
    url: https://support.google.com/mail/answer/145919
  - title: imgd.dev OpenAPI document
    url: https://imgd.dev/openapi.json
---

# Host Images for HTML Email Without Base64

Upload the image before email delivery, then put the returned public URL in the HTML `img` element. Include useful `alt`, `width`, and `height` attributes. This method keeps base64 image bytes out of the message body.

imgd.dev costs a one-time **$1 per GB** of storage and has **no free tier**. Fund the account before the preparation upload. Every imgd.dev image is public. Do not use this method for private offers, customer records, or confidential attachments.

Image hosting does **not** improve email deliverability. Sender authentication, reputation, content, and recipient policy still control delivery.

## Prepare one email image

Create the final raster image before upload. This example uses a 600 by 315 PNG, so the HTML dimensions match the file.

Keep `IMGD_KEY` outside the email project and its source files. Run this preparation step once:

```bash
set -euo pipefail
test -n "$IMGD_KEY"

response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
http_status="$(
  curl --silent --show-error \
    --output "$response_file" \
    --write-out '%{http_code}' \
    -X POST https://imgd.dev/v1/upload \
    -H "Authorization: Bearer $IMGD_KEY" \
    -F "file=@email-assets/monthly-report.png;type=image/png"
)"

if [[ "$http_status" != "200" && "$http_status" != "202" ]]; then
  jq . "$response_file" >&2
  exit 1
fi

cp "$response_file" upload.json
jq . upload.json
```

A new image returns HTTP `202` with this complete JSON shape:

```json
{
  "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "url": "https://i.imgd.dev/i/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "processing",
  "mime": "image/png",
  "bytes": 18452,
  "width": 600,
  "height": 315,
  "alt_text": null,
  "unpublish_at": null,
  "note": "the url works immediately, serving a blurred placeholder until the moderation check finishes"
}
```

An identical upload can return HTTP `200`. That response also contains `"deduplicated": true`.

The URL works immediately and can show a blurred image during moderation. For an email campaign, wait for a `live` metadata result before you send.

## Save only the public URL

Validate the returned URL before you put it in a template:

```bash
IMAGE_URL="$(
  jq -er \
    '.url | select(test("^https://i\\.imgd\\.dev/i/[0-9a-f]{64}$"))' \
    upload.json
)"
printf '%s\n' "$IMAGE_URL"
```

Store `IMAGE_URL` in normal template configuration. The URL is public and is not a secret.

Do not store `IMGD_KEY` with the template. The email renderer and sender do not need the upload key.

## Add the HTML image

Use a complete absolute URL because an email has no reliable relative asset base.

```html
<img
  src="https://i.imgd.dev/i/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  width="600"
  height="315"
  alt="Line chart of monthly API requests from January through June"
  style="display:block;width:600px;max-width:100%;height:auto;border:0;"
>
```

The `alt` text supplies a useful replacement when the image is blocked or unavailable. The `width` and `height` values also reserve the intended image area.

Keep important information in live email text. Do not put a required price, deadline, or action only inside the image.

Provide a plain-text email part too. Put the report text or a normal report link in that part.

## Verify before email delivery

Verify the image response from a normal HTTP client:

```bash
curl --fail --silent --show-error \
  -D image.headers \
  -o email-image.png \
  "$IMAGE_URL"
grep -i '^content-type: image/' image.headers
```

Then use an existing email test process:

1. Send to test mailboxes that your team already controls.
2. Check desktop, mobile, and web clients.
3. Check with remote images enabled.
4. Check with remote images blocked.
5. Confirm that the alt text remains useful.
6. Confirm that the layout stays readable without the image.

Some clients proxy, cache, preload, or block remote images. A remote image request does not reliably prove that a person read the message.

## Verify moderation before a campaign

Read the metadata until the status leaves `processing`. Use a bounded check in your preparation process.

```bash
HASH="$(jq -r '.hash' upload.json)"
for attempt in $(seq 1 20); do
  curl --fail --silent --show-error \
    -H "Authorization: Bearer $IMGD_KEY" \
    "https://imgd.dev/v1/images/$HASH" > metadata.json
  status="$(jq -r '.status' metadata.json)"
  case "$status" in
    live)
      jq . metadata.json
      break
      ;;
    review|blocked|error)
      jq . metadata.json >&2
      exit 1
      ;;
    processing)
      if [[ "$attempt" == "20" ]]; then
        echo "Moderation did not finish within 60 seconds." >&2
        exit 1
      fi
      sleep 3
      ;;
    *)
      echo "Unexpected image status: $status" >&2
      exit 1
      ;;
  esac
done
```

A live result can contain this JSON:

```json
{
  "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "url": "https://i.imgd.dev/i/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "live",
  "mime": "image/png",
  "bytes": 18452,
  "width": 600,
  "height": 315,
  "alt_text": "A line chart that shows monthly API requests rising from January through June.",
  "category": "safe",
  "nsfw": false,
  "violence": false,
  "filename": "monthly-report.png",
  "unpublish_at": null,
  "published": true,
  "created_at": 1787184000
}
```

Do not send a campaign with `processing`, `review`, `blocked`, or `error` content. A `review` state is terminal and stays blurred.

## Privacy implications

A recipient's email client or image proxy can request each remote image URL. That request can expose time and network metadata to systems that process access logs.

Apple Mail Privacy Protection can preload remote content through a proxy. Gmail also serves external images through its image infrastructure.

These client behaviors make open inference unreliable. They do not make a public image private.

Use the same image URL for all recipients when you do not need per-recipient tracking. Do not add an email address, customer ID, or secret to the query string.

Document your email privacy practice and follow the rules that apply to your recipients. This guide does not add a tracking pixel.

## Recover from upload errors

| Status | Meaning | Recovery |
| --- | --- | --- |
| `401` | The upload key is missing or invalid. | Set the correct `IMGD_KEY` in the preparation environment. |
| `402` | The account has no storage. | Buy storage, confirm funding, and retry. There is no free tier. |
| `402` | The account lacks quota. | Add the suggested storage or delete unused images, then retry. |
| `413` | The email image exceeds 20 MB. | Resize or compress the image before upload. |
| `415` | The media type is not accepted. | Use JPEG, PNG, GIF, WebP, or AVIF with the matching type. |
| `429` | The account exceeded the upload rate. | Wait for the next minute, then retry. |

A representative `402` quota response includes direct recovery data:

```json
{
  "error": "quota_exceeded",
  "fix": "this 18.0 KB upload needs more room than the 11.5 KB left on your 1 GB quota; POST https://imgd.dev/v1/credit with {\"gb\":1} to buy 1 GB for $1.00, then retry this upload, or DELETE images you no longer need",
  "topup_url": "https://imgd.dev/v1/credit",
  "suggested_gb": 1,
  "gb_price_usd": "1",
  "incoming_bytes": 18452,
  "storage_bytes": 1073730000,
  "storage_quota_bytes": 1073741824,
  "storage_remaining_bytes": 11824
}
```

Do not retry a `401`, `413`, or `415` response without the required change. Surface the API `fix` value to the operator.

## Security notes

- Keep `IMGD_KEY` in a preparation secret store.
- Do not send the key to the email renderer or delivery service.
- Do not put the key in HTML, query strings, logs, or source files.
- Remove sensitive metadata from the image before upload.
- Treat the image and URL as public.
- Do not encode recipient identifiers in the image URL.
- Keep essential content outside the image.

## When imgd.dev fits

imgd.dev fits a public logo, chart, diagram, or product image that needs a stable email URL. One preparation upload can support many messages.

It does not fit private recipient content, unique confidential statements, a free trial, video, or files above 20 MB. Use controlled delivery for those needs.

## Next guides

- [Add hosted images to Markdown and README files](/blog/host-images-markdown-readme/).
- [Resize and convert a hosted image](/blog/resize-convert-image-webp-avif/).
- [Upload a Python chart and get embed snippets](/blog/python-chart-public-url/).

For an agent, follow the [integration procedure](/integrate.md). For human setup, open [the start page](/#start). Read the [service evaluation](/evaluate.md) before you select a host.

## Sources

- [MDN HTML img element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img)
- [Apple Mail Privacy Protection](https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios)
- [Gmail external image settings](https://support.google.com/mail/answer/145919)
- [imgd.dev OpenAPI document](https://imgd.dev/openapi.json)
