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:

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:

{
  "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:

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.

<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:

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.

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:

{
  "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:

{
  "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

For an agent, follow the integration procedure. For human setup, open the start page. Read the service evaluation before you select a host.

Sources