# 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 2026-11-08 · https://shareglint.com/en/guides/mailchimp-personalized-images

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](https://shareglint.com/en/use-cases/personalized-email-images).

## 1. Build the URLs

```python
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](https://shareglint.com/en/guides/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](https://mailchimp.com/help/manage-audience-signup-form-fields/)). 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](https://mailchimp.com/help/import-contacts-mailchimp/)). 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:

```html
<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](https://mailchimp.com/help/all-the-merge-tags-cheat-sheet/)). 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](https://mailchimp.com/help/getting-started-with-merge-tags/)). 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](https://shareglint.com/en/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.
