Menu
Sign up

Personalized images in Mailchimp: one image per contact, from an audience field

Put a different image in each Mailchimp email: a signed image URL per contact in an audience field, the 255-byte limit, the code block and how to test it.

Published November 8, 2026 · 5 min read

A personalized image in an email is the same picture for everyone with one detail changed: the reader's name on a certificate, their own discount code on a coupon, their seat on a ticket. Mailchimp cannot draw such an image, but it can put a different image URL into each email. This page shows the setup end to end — the URL per contact, the audience field, the code block, and testing — and the one Mailchimp limit that matters.

What we checked. On 2026-11-08 we ran the script below on a three-contact CSV against our API: each generated URL answered with a redirect to its own rendered image. Lengths below are for URLs on our production address. Mailchimp's behaviour is quoted from its help pages read the same day; we did not send a Mailchimp campaign.

How it fits together

  1. A script builds one signed image URL per contact: the template plus that contact's values, signed with your secret.
  2. The URLs go into Mailchimp as an audience field (we call it CARD_URL), imported with the contacts.
  3. The email has an image whose source is the field's merge tag, *|CARD_URL|*.
  4. When the email is opened, the image is fetched from that URL. The first fetch renders it; every later fetch is served from cache.

The merge tag holds the whole URL, not a piece of it. Mailchimp fills merge tags when it sends but cannot compute the signature that protects the URL, so …?name=*|FNAME|*&s=… would fail. The reasoning is in personalized email images.

1. Build the URLs

import csv, hashlib, hmac, os, sys
from urllib.parse import quote

BASE = "https://shareglint.com"
ACCOUNT = os.environ["SHOT_ACCOUNT_ID"]
SECRET = os.environ["SHOT_SIGNING_SECRET"]
TEMPLATE = "certificate"
LIMIT = 255  # Mailchimp: audience field values are limited to 255 bytes

def signed_url(fields):
    path = f"/i/{ACCOUNT}/{TEMPLATE}.png"
    query = "&".join(f"{quote(k, safe='~')}={quote(v, safe='~')}" for k, v in sorted(fields.items()))
    s = hmac.new(SECRET.encode(), f"{path}?{query}".encode(), hashlib.sha256).hexdigest()[:32]
    return f"{BASE}{path}?{query}&s={s}"

with open("contacts.csv", newline="", encoding="utf-8") as src, \
     open("contacts-with-cards.csv", "w", newline="", encoding="utf-8") as out:
    rows = csv.DictReader(src)
    writer = csv.DictWriter(out, fieldnames=[*rows.fieldnames, "CARD_URL"])
    writer.writeheader()
    for row in rows:
        url = signed_url({
            "name": f"{row['First Name']} {row['Last Name']}",
            "course": row["Course"],
            "date": "8 November 2026",
        })
        if len(url.encode()) > LIMIT:
            print(f"{row['Email Address']}: {len(url.encode())} bytes, too long for a Mailchimp field", file=sys.stderr)
        writer.writerow({**row, "CARD_URL": url})

contacts.csv is a Mailchimp audience export (Email Address, First Name, Last Name, plus your own columns). The signature rule: path, ?, the fields sorted by name and RFC 3986-encoded, HMAC-SHA256 with your signing secret, first 32 hex characters (signed image URLs). Keep the secret on your machine or server; it never goes into Mailchimp.

2. Mind the 255 bytes

Mailchimp's help on audience fields: "Audience field values are limited to a maximum of 255 bytes of information" (Mailchimp). A signed URL is the address, the account, the template, every field value and the signature, so long values add up. Our three contacts:

Contact Fields in the URL Length
Anna Kowalska, "Advanced SQL" name, course, date 174 bytes
Łucja Żółkiewska-Grzęda, "Advanced SQL" name, course, date 209 bytes
Maximilian Featherstonehaugh-Montgomery, "Introduction to Data Engineering with Python and Apache Spark" name, course, date 263 bytes — too long

Letters outside ASCII cost the most: "Ż" is two bytes in UTF-8 and six characters once encoded in a URL (%C5%BB), which is why Łucja's URL is 35 bytes longer than Anna's for a name of similar length. Ways to stay under the limit:

  • Move fixed text into the template. If every certificate carries the same date and issuer, set them in your copy of the template; only what changes per contact goes in the URL. Dropping the date alone took the long URL from 263 to 238 bytes.
  • Shorten values that you control, such as course codes instead of full titles.
  • Redirect from your own short link. Store https://example.com/c/1842 in the field and let your server answer with a redirect to the signed URL.

The script prints every contact that goes over, so nothing is cut off silently.

3. Import the field

Add an audience field of type Text named, for example, "Card URL", with the merge tag CARD_URL, then import contacts-with-cards.csv and map the new column to it (Mailchimp: import contacts). Mailchimp also has an Image field type ("Contacts provide an image URL"), intended for contacts' own avatars; a Text field holding the URL is the plain choice for generated images.

4. Put the merge tag in the email

Use a Code content block (or a custom-coded template), where you control the HTML, and write the image tag yourself:

<img src="*|CARD_URL|*" alt="Your certificate for the Advanced SQL course" width="600" style="display:block;max-width:100%;height:auto;border:0;">

Mailchimp's own merge tag list describes this pattern for its logo tag: "Use this merge tag in place of an image URL in a custom-coded template or Code content block" (Mailchimp merge tags). The width keeps the layout stable while the image loads; the alt text is what readers see when images are blocked, so make it say what the image says.

5. Test before sending

From Mailchimp's guide to merge tags: "Merge tags connected to audience fields are contact-specific, meaning they won't populate in a test send." So a plain test email shows a broken image. Instead, "toggle on the Enable live merge tag info slider in Preview Mode, or send a live email to a small test segment of your audience" (Mailchimp). Put yourself in the audience with your own CARD_URL and send to a segment of one.

To spare the first readers the render time, open each URL once before the send (a loop over the CSV with curl -s -o /dev/null); after that every open is a redirect to a stored file.

Cost and limits

  • One image per contact is one render of the quota, charged at the first fetch; opens after that are served from cache. A campaign to 2,000 contacts is at most 2,000 renders: one for each contact whose image is ever fetched, all of them if you warm the URLs before sending. Check your plan on pricing. The Free plan's images carry a watermark, which a sent campaign should not have.
  • The image is made once, not at open time: a countdown or live stock level will not update. Text that changes after the send needs a new URL.
  • There is no Mailchimp app; the CSV import is the bridge, and the same CSV works for other mailing tools with their own merge tag syntax.

Other templates work the same way: coupon-code with offer, code and expires (154 bytes in our test), event-ticket with the attendee and seat, or a template you design yourself.

FAQ

How do I add a personalized image to a Mailchimp email?

Store each contact's image URL in an audience field (for example CARD_URL), then use its merge tag as the image source in a Code content block: <img src="*|CARD_URL|*" alt="Your certificate" width="600">. Mailchimp fills in each contact's URL when it sends, and every contact sees their own image.

Why can't I put *|FNAME|* inside the image URL?

Because a signed image URL carries a signature computed over its exact field values, and Mailchimp fills merge tags at send time without being able to sign. The URL with a merge tag inside would not match its signature. Compute the whole URL per contact beforehand and store it in a field.

Is there a length limit for the image URL in Mailchimp?

Yes: Mailchimp limits audience field values to 255 bytes. Our signed certificate URLs measured 174 to 263 bytes for three contacts; the longest name with a long course title went over. Fewer fields in the URL (put fixed text in the template) brings it back under.

Why does my test email show no image?

Mailchimp's docs say merge tags connected to audience fields "won't populate in a test send". Use Preview Mode with "Enable live merge tag info" switched on, or send the campaign to a small segment of real contacts, yourself included.